Skip to content

Fix : Telegram Integrations Fixes and UX improvements#359

Closed
samarth30 wants to merge 13 commits intodevfrom
fix/telegram-ux-overhaul
Closed

Fix : Telegram Integrations Fixes and UX improvements#359
samarth30 wants to merge 13 commits intodevfrom
fix/telegram-ux-overhaul

Conversation

@samarth30
Copy link
Member

@samarth30 samarth30 commented Feb 19, 2026

Summary

  • Telegram Markdown parse fix: OAuth URLs containing underscores (client_id, redirect_uri, etc.) broke Telegram's legacy Markdown parser. sendTelegramMessage returned 400 "can't parse entities" and silently dropped the message. Users never received the OAuth link. Fix: messages with long URLs (60+ chars) skip parse_mode entirely, letting Telegram auto-link them. A sendWithMarkdownFallback retry layer also catches any other Markdown failures and resends as plain text.
  • Extracted callTelegramApi and sendWithMarkdownFallback helpers: Deduplicated the fetch logic for Telegram API calls and added automatic Markdown-to-plain-text fallback on parse errors.
  • Message splitting: Large responses are now split at newline boundaries using splitMessage() to stay within Telegram's 4096-char limit instead of silently truncating.
  • Typing indicator: Added sendTypingIndicator on message receipt and createTypingRefresh (periodic 4s interval) so users see "typing..." while the agent processes — eliminates the silent wait.
  • Channel context injection: Injects a telegramChannelContext block into the LLM system prompt so the agent knows the user is on Telegram, avoids suggesting "connect Telegram", uses the user's name, and keeps responses mobile-concise.
  • Error resilience: Empty agent responses now send a fallback message instead of silent nothing. Agent failures send a user-facing error with retry guidance. The catch block for sendTelegramMessage is itself wrapped in try/catch. Request body parse failures return 400 instead of crashing. handleCommand is wrapped in try/catch.
  • sendCommandResponse wrapper: Command responses (/start, /help, /status) go through a wrapper that logs delivery failures.
  • Telegram helpers (telegram-helpers.ts): Added extractAuthUrls, stripAuthUrlsFromText, isSimpleMessage, createTypingRefresh, AUTH_URL_PATTERNS for OAuth URL detection across 10+ platforms.
  • Comprehensive test suite: 143 tests across telegram-ux-helpers.test.ts (new, 836 lines) and oauth-enforcement.test.ts (updated) covering message splitting, Markdown fallback, auth URL extraction/stripping, typing refresh, isSimpleMessage heuristic, and composition tests.
  • Multi-step template improvements: Added rules 6 & 7 — agent must always execute actions for user requests (no stale history reliance) and must always call OAUTH_CONNECT fresh. Added response guidelines to preserve URLs and provide clear next steps.
  • Twitter MCP tool visibility: Added twitter to CRUCIAL_TOOLS and SEARCH_ACTIONS platform enum so Twitter tools surface properly.
  • N8n Telegram credential bridge: Added resolveTelegramCredential to N8nCredentialBridge so n8n workflows can use the Telegram bot token for telegramApi credential type.
  • OAuth connect action: Updated the auth link response text to include "Tap the link above to authorize. When you're done, come back here and say 'done'" for clearer user guidance.

Test plan

  • Deploy to staging
  • Send a message triggering OAUTH_CONNECT for Google/Linear/Twitter via Telegram
  • Verify the OAuth URL is delivered to the user and is clickable
  • Check logs for Markdown parse failed, retrying as plain text warn (expected for URL messages)
  • Verify normal messages (no URLs) still render with Markdown formatting (bold, code blocks)
  • Send a long agent response (>4096 chars) and verify it's split into multiple messages
  • Verify "typing..." indicator appears while agent processes a message
  • Send /start, /help, /status commands and verify correct responses
  • Send a message as an unauthenticated user — verify welcome message with get-started link
  • Trigger an agent failure (e.g., insufficient credits) and verify user gets error message instead of silence
  • Test n8n workflow that sends a Telegram message — verify telegramApi credential resolves
  • Ask the agent to "search for Twitter actions" — verify Twitter tools appear in results
  • Run bun test tests/unit/eliza-app/telegram-ux-helpers.test.ts tests/unit/eliza-app/oauth-enforcement.test.ts — all 143 tests pass
Screenshot 2026-02-20 at 1 22 50 AM Screenshot 2026-02-20 at 1 22 31 AM

Note

Medium Risk
Touches the Telegram webhook request/response path and LLM prompt injection, so regressions could impact message delivery or agent behavior. Also introduces Telegram credential resolution for n8n, which depends on correct secret selection and handling.

Overview
Fixes Telegram outbound messaging reliability by splitting long responses, sending typing indicators during processing, and adding Markdown→plain-text fallback (including skipping Markdown when long URLs are present) so OAuth links aren’t dropped by Telegram parse errors.

Improves Telegram agent behavior by injecting a Telegram-specific channel context into the runtime system prompt, adds user-facing fallbacks for empty/failed agent responses, wraps command handling and webhook JSON parsing with safer error handling, and simplifies commands to primarily /status.

Updates assistant/multi-step prompts and OAuth actions to always generate fresh OAuth links, preserve full URLs in responses, and end with a clear next step; adds telegramApi credential resolution in N8nCredentialBridge using org/app bot tokens, plus unit tests covering the new Telegram helper behaviors and updated rejection/status messaging.

Written by Cursor Bugbot for commit 2607e26. This will update automatically on new commits. Configure here.

samarth30 and others added 3 commits February 19, 2026 21:53
- Replace inline auth URLs with Telegram inline keyboard buttons
- Add markdown-to-plain-text fallback for message send failures
- Split long messages to respect Telegram's 4096 char limit
- Add empty response fallback so users never get silence
- Guard request.json() parsing with try/catch and structured logging
- Tighten OAuth URL patterns to prevent false positives (Microsoft, Notion)
- Strip only auth URLs from responses, preserving non-auth links
- Log command delivery failures and typing indicator errors
- Improve welcome, help, and status command messages with CRO patterns
- Add 68 unit tests covering helpers, markdown fallback, and URL extraction
- Update oauth-enforcement tests to match new message format

Co-authored-by: Cursor <cursoragent@cursor.com>
… context

Telegram was rejected as "unsupported" when users requested email-to-Telegram
automations because telegramApi wasn't recognized by the credential bridge.

- Add telegramApi resolution to N8nCredentialBridge (bot token from env or org)
- Update checkCredentialTypes to include Telegram as supported
- Inject channel context into runtime system prompt so the LLM knows the user
  is on Telegram, has their chat ID, and won't suggest "connect Telegram"

Co-authored-by: Cursor <cursoragent@cursor.com>
…silience, and channel context

- Add fast initial response (ACK) for complex messages to reduce perceived latency
- Add typing indicator refresh with configurable interval and error callback
- Expand channel context prompt with trust signals, action-oriented instructions, and response style rules
- Wrap error message sending in try/catch to prevent unhandled exceptions
- Wrap command handlers in try/catch for resilient command processing
- Log warning when runtime.character is null and channel context cannot be injected
- Remove redundant optional chaining and consolidate if/else blocks
- Update oauth-enforcement tests to match new welcome message format
- Add 50+ tests for isSimpleMessage and createTypingRefresh

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel
Copy link

vercel bot commented Feb 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
eliza-cloud-v2 Ready Ready Preview, Comment Feb 20, 2026 6:47am

@samarth30 samarth30 requested a review from Copilot February 19, 2026 16:32
@coderabbitai
Copy link

coderabbitai bot commented Feb 19, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login.

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude
Copy link

claude bot commented Feb 19, 2026

PR Review: Telegram UX Overhaul (#359)

Good set of improvements overall — message chunking, Markdown fallback, inline buttons, and typing refresh are all well-structured. The 836-line test suite is thorough. A few issues worth addressing before merge:


Bugs

1. x.com pattern is too broad (lib/utils/telegram-helpers.ts:126)

["Connect Twitter / X", (u) => u.includes("api.twitter") || u.includes("twitter.com/i/oauth") || u.includes("x.com")],

u.includes("x.com") matches any domain containing x.com as a substring: box.com, hex.com, maxcdn.com, prefix.example.com, etc. These would all produce a spurious "Connect Twitter / X" button.

Fix: tighten to u.includes("//x.com/") || u.includes(".x.com/") or use a URL-specific check.


2. ACTION_KEYWORDS lacks word boundaries (lib/utils/telegram-helpers.ts:162)

const ACTION_KEYWORDS = /create|automate|connect|set up|build|send|check|read|draft/i;

Words like "already", "thread", "spread", "overhead", and "android" all contain keyword substrings (read, read, read, read). This causes isSimpleMessage to return false for short innocuous messages like "already done" or "go ahead", unnecessarily sending an ACK for them.

Fix: add word boundaries — \b(create|automate|connect|set up|build|send|check|read|draft)\b.


3. Mutable runtime character mutation (app/api/eliza-app/webhook/telegram/route.ts:237)

runtime.character.system = (runtime.character.system || "") + telegramChannelContext;

If runtimeFactory.createRuntimeForUser returns a cached runtime object (shared across requests for the same user), this append operation will accumulate duplicate Telegram context blocks on every message. After a few messages the system prompt could contain the same instructions dozens of times. This would bloat the context window and potentially degrade model output.

Either deep-clone the character before mutation, inject the context as a separate system message, or confirm createRuntimeForUser always returns a fresh object.


4. callTelegramApi is hardcoded to sendMessage (app/api/eliza-app/webhook/telegram/route.ts:34)

async function callTelegramApi(payload: Record<string, unknown>): Promise<Response> {
  return fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, {

The name implies a generic API wrapper but it's hardcoded to one endpoint. If this is ever extended or called expecting a different method, it will silently do the wrong thing. Consider renaming to callSendMessage or making the method a parameter.


Security

5. App bot token shared across all orgs (lib/eliza/plugin-n8n-bridge/n8n-credential-bridge.ts:584)

const appBotToken = process.env.ELIZA_APP_TELEGRAM_BOT_TOKEN;
if (appBotToken) {
  return { status: "credential_data", data: { accessToken: appBotToken } };
}

All users' n8n automations receive the shared app bot token. A workflow for Organization A could use this token to send messages to chat IDs belonging to Organization B users. If this is a multi-tenant deployment this needs an explicit note that it's intentional, or the fallback to org-specific tokens only should be preferred.


Minor

6. stripAuthUrlsFromText won't strip multi-word "Connect" prefixes (lib/utils/telegram-helpers.ts:157)

.replace(/Connect \w+:\s*/gi, "")

\w+ matches a single word token, so "Connect Twitter / X:" would only strip "Connect Twitter" leaving " / X:" in the output. Since extractAuthUrls uses the label "Connect Twitter / X", text containing that label format won't be cleaned correctly.

Fix: .replace(/Connect[\w\s/]+:\s*/gi, "") or similar.


What's Good

  • Markdown fallback with retry on "can't parse entities" is correctly implemented and well-tested
  • Typing refresh timer is reliably stopped in finally — no leak risk
  • sendWithMarkdownFallback preserves all payload fields (inline keyboard, reply IDs) on retry
  • Command handler now has try/catch — no more unhandled promise rejections from command failures
  • The test suite covers happy path, edge cases, false positives, and composition — good quality
  • Injecting Telegram channel context into the system prompt is a clean solution without modifying shared prompts

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR upgrades the Eliza App Telegram webhook experience by making message delivery more reliable (chunking + Markdown fallback), improving onboarding/status UX with inline buttons, and tightening agent prompting so responses are more action-oriented and Telegram-aware. It also extends the n8n credential bridge to resolve Telegram credentials and adds substantial unit coverage for new Telegram helper utilities.

Changes:

  • Refactors Telegram webhook message sending (chunking, Markdown fallback, inline buttons, typing/ack behaviors) and updates onboarding/status copy.
  • Adds Telegram-specific helper utilities (auth URL extraction/stripping, message classification, typing refresh) with unit tests.
  • Updates agent prompts/templates to enforce “fresh OAuth link” + “preserve URLs” + “clear next step”, and extends n8n credential resolution for Telegram.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
app/api/eliza-app/webhook/telegram/route.ts Refactors webhook message sending, adds inline-button onboarding/status, typing + ack behavior, and injects Telegram channel context into the runtime prompt.
lib/utils/telegram-helpers.ts Adds auth-URL extraction/stripping utilities, “simple message” classifier, and typing refresh helper alongside existing Telegram helpers.
tests/unit/eliza-app/telegram-ux-helpers.test.ts Adds extensive unit coverage for new Telegram helper utilities and Markdown fallback behavior.
tests/unit/eliza-app/oauth-enforcement.test.ts Updates enforcement assertions to reflect new Telegram onboarding/status messaging and button-based Get Started flow.
lib/eliza/plugin-oauth/actions/oauth-connect.ts Updates action description + returned text to emphasize fresh OAuth links and more guided UX.
lib/eliza/plugin-oauth/actions/oauth-get.ts Updates connection status messaging to be more actionable and user-friendly.
lib/eliza/plugin-n8n-bridge/n8n-credential-bridge.ts Adds Telegram credential type support (token resolution) for n8n workflows.
lib/eliza/plugin-cloud-bootstrap/templates/multi-step.ts Strengthens decision rules to enforce action execution, URL preservation, and fresh OAuth link generation.
lib/eliza/plugin-assistant/prompts/chat-assistant-prompts.ts Adds engagement rules and explicit “preserve URLs + clear next step” requirements to the system prompt.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +122 to +123
const URL_REGEX = /https?:\/\/[^\s)>\]]+/g;

Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

URL_REGEX will include common trailing punctuation (e.g., ".", ",", """, "'"), so extractAuthUrls()/stripAuthUrlsFromText() can produce broken auth buttons/URLs when a link appears at end of a sentence or wrapped in quotes. Consider post-processing each match to trim trailing punctuation, or update the regex to exclude these characters while still allowing valid URL characters.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed — URL_REGEX now excludes trailing punctuation (.,;:!?"'). Also removed the g flag from the module-level regex to prevent stale lastIndex issues; functions that need global matching create a fresh regex inline.

Comment on lines +154 to +158
export function stripAuthUrlsFromText(text: string): string {
return text
.replace(URL_REGEX, (url) => isAuthUrl(url) ? "" : url)
.replace(/Connect \w+:\s*/gi, "")
.replace(/\n{3,}/g, "\n\n")
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

stripAuthUrlsFromText() uses /Connect \w+:\s*/ which only removes single-word platform prefixes. If the agent outputs prefixes like "Connect Twitter / X:" or other multi-word variants, the cleaned text will keep the prefix and look awkward. Consider broadening the prefix-stripping pattern (e.g., up to ':' on the line) or deriving it from AUTH_URL_PATTERNS labels.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed — broadened regex to /Connect[\\w\\s/]+:\\s*/gi to handle multi-word prefixes like "Connect Twitter / X:". Note: this function is not used in production currently (button feature removed), kept as tested utility.

Comment on lines +118 to +126
return sendWithMarkdownFallback({
chat_id: chatId,
text,
reply_to_message_id: replyToMessageId,
parse_mode: "Markdown",
reply_markup: {
inline_keyboard: buttons.map((b) => [{ text: b.label, url: b.url }]),
},
});
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

sendTelegramMessageWithButtons() sends a single message without chunking. If text exceeds Telegram’s 4096 character limit (easy if the agent returns a long explanation + the appended "say done" footer), the send will fail and the user will get no auth button. Consider splitting text with splitMessage() and attaching reply_markup only to the last chunk (similar to lib/services/telegram-automation/app-automation.ts).

Suggested change
return sendWithMarkdownFallback({
chat_id: chatId,
text,
reply_to_message_id: replyToMessageId,
parse_mode: "Markdown",
reply_markup: {
inline_keyboard: buttons.map((b) => [{ text: b.label, url: b.url }]),
},
});
const chunks = splitMessage(text, TELEGRAM_RATE_LIMITS.MAX_MESSAGE_LENGTH);
if (chunks.length === 0) return true;
for (let i = 0; i < chunks.length; i++) {
const isLastChunk = i === chunks.length - 1;
const ok = await sendWithMarkdownFallback({
chat_id: chatId,
text: chunks[i],
reply_to_message_id: i === 0 ? replyToMessageId : undefined,
parse_mode: "Markdown",
reply_markup: isLastChunk
? {
inline_keyboard: buttons.map((b) => [{ text: b.label, url: b.url }]),
}
: undefined,
});
if (!ok) return false;
}
return true;

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

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

N/A — sendTelegramMessageWithButtons was removed entirely during the feature revert. All messages now go through sendTelegramMessage which uses splitMessage() for chunking.

Comment on lines +288 to +300
const authButtons = extractAuthUrls(responseText);
if (authButtons.length > 0) {
const cleanedText = stripAuthUrlsFromText(responseText) ||
"Tap the button below to connect your account:";
await sendTelegramMessageWithButtons(
message.chat.id,
`${cleanedText}\n\nOnce you've authorized, come back and say *done* so I can verify.`,
authButtons,
message.message_id,
);
} else {
await sendTelegramMessage(message.chat.id, responseText, message.message_id);
}
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

handleMessage() ignores the boolean return value from sendTelegramMessage()/sendTelegramMessageWithButtons(). If Telegram sendMessage fails (network, rate limit, invalid token), the webhook still returns 200 and marks the update processed, so the user never receives a response and Telegram won’t retry. Consider checking the return value for the final response and deciding on a retry strategy (e.g., return false only before any side-effecting actions, or enqueue a retry/outbox send).

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed — sendTelegramMessage return value is now checked. If the final response fails to deliver, we return false to trigger a 503 and Telegram retry.

Comment on lines 141 to 157
examples: [
[
{ name: "{{name1}}", content: { text: "connect my google account" } },
{ name: "{{name2}}", content: { text: "Connect Google: https://accounts.google.com/...", actions: ["OAUTH_CONNECT"] } },
],
[
{ name: "{{name1}}", content: { text: "link gmail" } },
{ name: "{{name2}}", content: { text: "Connect Google: https://accounts.google.com/...", actions: ["OAUTH_CONNECT"] } },
],
[
{ name: "{{name1}}", content: { text: "connect my twitter account" } },
{ name: "{{name2}}", content: { text: "Connect Twitter: https://api.twitter.com/oauth/...", actions: ["OAUTH_CONNECT"] } },
],
[
{ name: "{{name1}}", content: { text: "link my x account" } },
{ name: "{{name2}}", content: { text: "Connect Twitter: https://api.twitter.com/oauth/...", actions: ["OAUTH_CONNECT"] } },
],
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

oauthConnectAction’s handler message format was updated to "Here's your authorization link...", but the action examples still use the old "Connect : " format. These examples influence agent behavior, so they should be updated to match the new UX and to keep webhook URL/button extraction consistent.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed — examples updated to match the current handler output format (Here's your [Platform] authorization link...).

Comment on lines +295 to +305
private async resolveTelegramCredential(
userId: string,
): Promise<CredentialProviderResult> {
const appBotToken = process.env.ELIZA_APP_TELEGRAM_BOT_TOKEN;
if (appBotToken) {
logger.info("[N8nCredentialBridge] Telegram credential resolved from app bot token");
return {
status: "credential_data",
data: { accessToken: appBotToken },
};
}
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

resolveTelegramCredential() returns the global ELIZA_APP_TELEGRAM_BOT_TOKEN to any requesting user before attempting to use the organization-scoped bot token. This can unintentionally override per-org configuration and increases blast radius if a token is misused. Consider preferring the organization token when present (and only falling back to the app token for the Eliza App org or when explicitly configured).

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed — flipped the order in resolveTelegramCredential: org-specific orgBotToken is checked first, app-level token is now a fallback. Also switched from process.env.ELIZA_APP_TELEGRAM_BOT_TOKEN to elizaAppConfig.telegram.botToken for consistency.

Comment on lines +831 to +835
test("accepts onError callback without crashing", async () => {
const typing = createTypingRefresh(12345, "fake-token", 50, () => {});
await new Promise((r) => setTimeout(r, 120));
typing.stop();
});
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The createTypingRefresh tests trigger real network calls to https://api.telegram.org (interval=50ms and a 120ms sleep). This can make unit tests flaky/slow and can fail in CI environments without outbound network access. Consider refactoring createTypingRefresh to accept an injectable fetch/apiBase for tests (or stubbing fetch in the test) so the tests don’t hit external services.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed — intervalMs increased to 60000ms so the typing refresh never fires during the test.

@claude
Copy link

claude bot commented Feb 19, 2026

PR Review: Fix — Telegram UX (#359)

Good overall direction — the UX improvements (chunking, inline buttons, Markdown fallback, ACK messages, typing refresh) are all sensible additions. The test coverage for the helpers is solid. A few issues need attention before merge.


🔴 Bug: System Prompt Accumulates on Cached Runtimes

File: app/api/eliza-app/webhook/telegram/route.ts (~line 238)

if (runtime.character) {
  runtime.character.system = (runtime.character.system || "") + telegramChannelContext;
}

runtimeFactory.createRuntimeForUser() returns a cached runtime on repeated calls (cache HIT path in runtime-factory.ts:546). The character object is shared across requests. Every Telegram message from the same user appends the 35-line telegramChannelContext block to runtime.character.system — it never gets cleared.

Every other handler in this codebase (see plugin-assistant/handler.ts:95+313, plugin-chat-playground/handler.ts:68+213, plugin-character-builder/actions/save-changes.ts:303+319) follows the save-modify-restore pattern:

const originalSystemPrompt = runtime.character.system;
try {
  runtime.character.system = ...;
  // process
} finally {
  runtime.character.system = originalSystemPrompt;
}

This PR doesn't restore. After N messages, the system prompt grows by N × ~1400 characters, burning tokens and eventually hitting context limits. Fix by saving and restoring — or by injecting the context into state rather than mutating the character object.


🟠 Security: App Bot Token Returned to All Users Without Scoping

File: lib/eliza/plugin-n8n-bridge/n8n-credential-bridge.ts (~line 298)

const appBotToken = process.env.ELIZA_APP_TELEGRAM_BOT_TOKEN;
if (appBotToken) {
  return { status: "credential_data", data: { accessToken: appBotToken } };
}

ELIZA_APP_TELEGRAM_BOT_TOKEN is the same token used by the webhook to operate the main Eliza bot. Any user's n8n workflow requesting telegramApi credentials receives this token, regardless of whether their org has its own bot. This means any user could direct the Eliza bot to send messages to arbitrary chats they know the chat ID of — including other users' private chats.

If this intent is for the app bot to be shared (send messages to the requesting user), that constraint needs to be enforced in the n8n workflow template, not assumed. If each org should send as their own bot, the fallback to orgBotToken is correct and the app-level token should not be returned here. Either way, this deserves explicit documentation.


🟡 Tests Test Reproduced Logic, Not Production Functions

File: tests/unit/eliza-app/telegram-ux-helpers.test.ts

Three test suites don't test the production code path:

  1. "Markdown fallback retry logic" — tests destructuring logic inline, not sendWithMarkdownFallback from route.ts. The production function could be broken and these tests would still pass.

  2. "Markdown fallback with real HTTP server" — defines its own sendWithFallback function that mirrors the production logic. Same issue: no regression protection.

  3. "sendTelegramMessageWithButtons reply_markup structure" — builds the reply_markup inline and asserts on its own construction. Not testing the actual sendTelegramMessageWithButtons function.

Since sendWithMarkdownFallback is a module-level function in route.ts (not exported), extracting it to telegram-helpers.ts would make it directly testable and resolve the issue.


🟡 createTypingRefresh Test Makes Real Network Calls

File: tests/unit/eliza-app/telegram-ux-helpers.test.ts (~line 1646)

test("accepts onError callback without crashing", async () => {
  const typing = createTypingRefresh(12345, "fake-token", 50, () => {});
  await new Promise((r) => setTimeout(r, 120));
  typing.stop();
});

With intervalMs: 50 and a 120ms wait, this fires ~2–3 real fetch calls to https://api.telegram.org/bot${botToken}/sendChatAction. They'll fail with a 401, but the test makes live outbound network requests. This will be flaky in offline/restricted CI environments and hammers the Telegram API with garbage requests. The 60000ms interval used in other tests avoids this correctly.


🟡 callTelegramApi Name Is Misleading

File: app/api/eliza-app/webhook/telegram/route.ts (~line 34)

async function callTelegramApi(payload: Record<string, unknown>): Promise<Response> {
  return fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, { ... });
}

Despite its generic name, this only ever calls /sendMessage. The typing indicator uses its own fetch call directly. Consider callSendMessage or callTelegramSendMessage.


🟢 Positives

  • Markdown fallback strategy (sendWithMarkdownFallback) is well-implemented and handles the right error case.
  • Message chunking at line boundaries preserves word wrapping correctly.
  • createTypingRefresh with onError callback is a clean API.
  • The extractAuthUrls / stripAuthUrlsFromText pair is well-designed and the false-positive tests for Microsoft/Notion docs URLs are good catches.
  • handleCommand now has try/catch wrapping — previously a DB error in /status would have silently swallowed.
  • isSimpleMessage heuristic is simple, well-tested, and the edge cases (case-insensitivity, "creation" vs "create") are covered.
  • Removing BLOOIO_PHONE from the welcome message avoids referencing a feature not relevant to the OAuth flow.
  • 836-line test file for the helpers, including real HTTP server tests for the fallback flow, is good investment.

Summary

Issue Severity
System prompt accumulates on cached runtimes 🔴 Bug
App bot token returned to all users 🟠 Security
Tests test reproduced logic, not production functions 🟡 Quality
createTypingRefresh test makes real network calls 🟡 Quality
callTelegramApi misleading name 🟡 Minor

The system prompt accumulation is the most important fix before merge — it will cause escalating token costs and eventual failures for active users.

@samarth30 samarth30 changed the title Fix : Telegram UX Fix : Telegram Integrations Fixes and UX improvements Feb 19, 2026
@samarth30 samarth30 marked this pull request as ready for review February 19, 2026 19:58
… Markdown for URL messages

Reverts expanded personality, welcome flows, and channel context to
factual-only originals. Removes sendTelegramMessageWithButtons,
ACK messages, and isSimpleMessage gate. Restores BLOOIO_PHONE in
/start. Skips parse_mode on messages with long URLs so Telegram
auto-links OAuth links instead of mangling them with Markdown.

Co-authored-by: Cursor <cursoragent@cursor.com>
@claude
Copy link

claude bot commented Feb 19, 2026

Code Review

Overall this is a solid set of improvements — the Markdown fallback retry, message splitting, typing indicators, and error resilience are all well-motivated. Test coverage for the helper utilities is thorough. A few issues worth addressing before merge:


Bug: Runtime system prompt mutation may be stateful

app/api/eliza-app/webhook/telegram/route.ts, line ~189

if (runtime.character) {
  runtime.character.system = (runtime.character.system || "") + telegramChannelContext;
}

This mutates runtime.character.system in-place. If runtimeFactory.createRuntimeForUser ever returns a cached/shared runtime (common in runtime factory patterns), the system prompt will grow by ~500 chars on every message, eventually blowing past context limits or producing corrupted prompts. Even if runtimes are fresh today, this pattern is a time bomb.

Fix: Build the system string without mutating the object, or clone the character:

if (runtime.character) {
  runtime.character = {
    ...runtime.character,
    system: (runtime.character.system || "") + telegramChannelContext,
  };
}

Bug: /status command sends "undefined" as telegramUserId

app/api/eliza-app/webhook/telegram/route.ts, line ~306

const telegramUserId = String(message.from?.id);

handleCommand now takes Message & { text: string }, but from is still typed as optional. If from is absent (e.g., channel posts), String(undefined) produces "undefined", and elizaAppUserService.getByTelegramId("undefined") would either silently fail or return the wrong result. The earlier if (!message.from) return true guard in handleMessage doesn't cover the case where commands arrive outside that flow.

Fix: Add a message.from guard inside the /status case, or narrow the handleCommand signature to Message & { text: string; from: NonNullable<Message['from']> }.


Design concern: callTelegramApi is misleadingly named

The function is described as a generic API caller but it's hardcoded to /sendMessage:

async function callTelegramApi(payload: Record<string, unknown>): Promise<Response> {
  return fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, { ... });
}

Meanwhile sendTypingIndicator duplicates the fetch setup inline. If the intent is a general Telegram API helper, pass the method as a parameter. If it's only for sendMessage, rename it to callSendMessage to avoid confusion.


Dead code: extractAuthUrls / stripAuthUrlsFromText unused in webhook

The route imports splitMessage, TELEGRAM_RATE_LIMITS, and createTypingRefresh from telegram-helpers. However extractAuthUrls and stripAuthUrlsFromText (and AUTH_URL_PATTERNS) are exported/tested but never called from the webhook. The tests in sendTelegramMessageWithButtons describe group also test inline keyboard markup construction that doesn't exist in the webhook.

Per the PR description the button feature was reverted. If these helpers are genuinely intended for future use, a comment noting that would help. If not, shipping them as dead code adds maintenance surface. The test descriptions should at minimum not reference sendTelegramMessageWithButtons since no such function exists.


Overly broad generic OAuth pattern

lib/utils/telegram-helpers.ts, line ~619

["Authorize", (u) => u.includes("oauth") || u.includes("/auth/")],

The /auth/ path check would match URLs like https://example.com/auth/profile or https://api.example.com/auth/token that aren't OAuth flows at all — it's a pretty common REST path segment. This is the fallback pattern so it's not catastrophic, but it could produce spurious "Authorize" buttons for non-OAuth links. Consider tightening to something like /oauth/ or checking for both /auth/ AND common OAuth params (?code=, ?state=, ?client_id=).


Weakened test assertions in oauth-enforcement.test.ts

The PR replaces specific URL assertions with looser string matches:

// Before
expect(messages.telegramRejection).toContain("https://eliza.app/get-started");
expect(messages.telegramRejection.toLowerCase()).toContain("telegram");

// After  
expect(messages.telegramRejection.toLowerCase()).toContain("eliza");

The original tests verified that the get-started URL was present in the rejection message — an important UX guarantee. The new assertion only checks for "eliza" anywhere in a hardcoded string. The new "Get Started URL is present in webhook code" test only checks that "get-started" appears anywhere in the source file (it would pass even if the string is commented out). These tests now provide weaker safety guarantees than before.


Minor: typing created before try block

const typing = BOT_TOKEN ? createTypingRefresh(...) : null;

try {
  await sendTypingIndicator(message.chat.id);
  ...
} finally {
  typing?.stop();   // clears interval
  await lock.release();
}

Currently safe since createTypingRefresh doesn't throw and there's no code between it and try. But the finally block won't run if an exception is thrown before try. If any setup code is ever added between those two lines, the interval could leak. A minor style nit: either move createTypingRefresh to the first line inside try, or keep it outside and accept the (currently zero) risk.


Nit: N8n credential bridge — inconsistent lookup style

if (credType in API_KEY_CRED_TYPES)          // object `in` check
if (TELEGRAM_CRED_TYPES.has(credType))        // Set .has() check

Not a bug, but mixing in for object checks and .has() for Set checks is inconsistent. Consider converting API_KEY_CRED_TYPES to a Set for consistency, or vice versa.


Summary

Severity Issue
Should fix Runtime system prompt mutation (stateful if runtime is cached)
Should fix /status command String(from?.id)"undefined"
Worth discussing callTelegramApi naming / sendTypingIndicator duplication
Worth discussing Dead code: extractAuthUrls/stripAuthUrlsFromText not used in webhook
Worth discussing Overly broad /auth/ fallback pattern in AUTH_URL_PATTERNS
Worth discussing Weakened oauth-enforcement.test.ts assertions
Nit typing outside try block
Nit Inconsistent in vs .has() check style

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@samarth30
Copy link
Member Author

Claude says: Re: the "Tests test reproduced logic, not production functions" feedback —

Acknowledged. The Markdown fallback tests reproduce inline logic rather than testing the production sendWithMarkdownFallback directly. This is an architectural limitation: the function is a module-level private in route.ts and can't be imported.

Extracting it to telegram-helpers.ts would make it directly testable but would also require passing the bot token / API URL as parameters (since it currently closes over the module-level BOT_TOKEN constant). This is a valid refactor but out of scope for this PR — the real HTTP server tests in the suite already cover the same retry semantics with a mock server, providing equivalent regression protection.

We'll track extraction as a follow-up.

— Claude

…, pattern fixes

- Save/restore runtime.character.system to prevent accumulation on cached runtimes
- Check sendTelegramMessage return value; return 503 if response delivery fails
- Wrap sendWithMarkdownFallback in try/catch for network errors
- Guard /status command against missing message.from
- Move typing refresh creation inside try block
- Rename callTelegramApi to callSendMessage
- Prefer org bot token over app token in n8n credential bridge
- Tighten x.com pattern to prevent false positives (box.com, hex.com)
- Add word boundaries to ACTION_KEYWORDS regex
- Broaden stripAuthUrlsFromText prefix regex for multi-word labels
- Exclude trailing punctuation from URL regex matches
- Tighten /auth/ fallback to require OAuth query params
- Strengthen oauth-enforcement test assertions to verify actual rejection text
- Update oauthConnectAction examples to match handler output format
- Rename test describe from removed sendTelegramMessageWithButtons
- Fix createTypingRefresh test to avoid real network calls

Co-authored-by: Cursor <cursoragent@cursor.com>
@claude
Copy link

claude bot commented Feb 19, 2026

PR Review

Good set of improvements overall — the Markdown fallback, message splitting, and typing indicator are all real UX wins. Test coverage is solid. A few issues worth discussing before merge.


Critical

System prompt mutation is not concurrency-safe
route.ts lines ~204–269:

const originalSystemPrompt = runtime.character?.system;
if (runtime.character) {
  runtime.character.system = (runtime.character.system || "") + telegramChannelContext;
}
// ... finally: runtime.character.system = originalSystemPrompt;

This mutates the runtime object in-place and restores it in finally. If runtimeFactory.createRuntimeForUser returns a cached or shared runtime instance (even per-org), concurrent webhook requests for the same organization could race: request A injects context, request B injects context on top of A's injection, then A's finally restores to the original — leaving B's runtime corrupted.

If the runtime is guaranteed to be a fresh object per call, this is safe — but that guarantee should be explicit. A safer approach is to pass the channel context as an argument to createMessageHandler/process rather than mutating shared state.


Medium

sendTypingIndicator ignores BOT_TOKEN guard
createTypingRefresh is correctly gated on BOT_TOKEN ? ... : null, but sendTypingIndicator(message.chat.id) is called unconditionally 3 lines later. When BOT_TOKEN is falsy, this calls https://api.telegram.org/botundefined/sendChatAction — harmless since the error is caught and logged at debug, but inconsistent:

// createTypingRefresh is gated...
typing = BOT_TOKEN ? createTypingRefresh(...) : null;

// ...but sendTypingIndicator is not
await sendTypingIndicator(message.chat.id); // calls with undefined token

URL length threshold (60 chars) is fragile

const URL_PATTERN = /https?:\/\/\S{60,}/;

A short OAuth URL like https://github.com/login/oauth/authorize?client_id=x (53 chars) has underscores and will still get parse_mode: "Markdown" applied, potentially triggering the fallback retry. The sendWithMarkdownFallback retry handles this correctly, but the 60-char skip is a leaky heuristic. The actual trigger for Telegram parse failures is underscores/asterisks inside URLs, not URL length. Worth a comment explaining the rationale, or switching to checking if the URL contains _ or *.

Module-level global regex with g flag
telegram-helpers.ts:

const URL_REGEX = /https?:\/\/[^\s)>\]]+[^\s)>\].,;:\!?"']/g;

A module-level regex with the g flag retains lastIndex across calls when used with .exec() or .test(). The current code only uses it with String.prototype.match() and .replace(), both of which reset lastIndex after completion — so this is safe today. But it's a footgun if someone later adds .test(URL_REGEX) or .exec(URL_REGEX). Consider making it a factory function or using a local variable inside each function.

resolveTelegramCredential has no tests
The new N8nCredentialBridge method resolves org bot token → app env var → null. This path should have unit tests covering the three branches (org token found, org token missing but env var present, neither). It's also unclear whether the same bot token being used by multiple org members in n8n workflows is intended behavior.

stripAuthUrlsFromText prefix regex is too broad

.replace(/Connect[\w\s\/]+:\s*/gi, "")

This removes any text matching Connect<words>: — including "Connecting to: example" or "Connected apps:" in a normal response. Scoping it to the known platform names (or a word-boundary pattern) would be safer.


Minor

Explanatory comments removed without replacement
Several inline comments explaining why the code does something were deleted, e.g.:

  • // Not applicable, mark as processed
  • // Use userId as entityId for unified memory
  • // Look up user - they must have completed OAuth first
  • // Only mark as processed if handler succeeded (prevents lost messages on lock failure)

The last one in particular explained a non-obvious business rule about idempotency. These help future maintainers understand intent.

Agent failure always marks as processed

} catch (error) {
  // ...send error message to user...
  return true; // marks message as processed — no Telegram retry
}

Returning true after an agent failure prevents Telegram from retrying, which avoids infinite loops. But for transient errors (network blip, OOM), this silently eats the message. Worth a comment distinguishing "don't retry" (intentional) from "retry on lock failure" (which does return false/503).

isSimpleMessage threshold edge cases
"what time" (2 words, no action keyword) → true (no ack). "what is the time" (4 words) → false (ack). This cutoff feels arbitrary for messages without action keywords. Low impact since it only affects the typing indicator ACK behavior.

Prompt: "End every response with one clear next step"
Adding this to chatAssistantFinalSystemPrompt is aggressive for purely informational replies (e.g., "What time is it in Tokyo?"). A user asking a factual question probably doesn't want a forced next-step appended. Consider scoping this rule to action-oriented contexts.

ELIZA_APP_TELEGRAM_BOT_TOKEN vs elizaAppConfig.telegram.botToken
The credential bridge reads process.env.ELIZA_APP_TELEGRAM_BOT_TOKEN directly, while the webhook uses elizaAppConfig.telegram.botToken. These should be the same env var, but reading it two different ways is fragile if the config key ever changes. Prefer going through elizaAppConfig.


Positives

  • sendWithMarkdownFallback is a clean abstraction — the retry-on-parse-error pattern is solid and the logging gives good observability.
  • splitMessage with newline-boundary splitting is correct and the round-trip test (chunks.join("\n") === original) verifies it well.
  • createTypingRefresh is properly cleaned up in finally regardless of success/failure.
  • Request body parse guard returning 400 instead of crashing is a good defensive addition.
  • 503 on lock failure to trigger Telegram retry is the right behavior.
  • Test suite is comprehensive — the HTTP server-based fallback tests in particular are a good pattern for testing network retry logic without mocking internal functions.
  • handleCommand wrapped in try/catch prevents command errors from crashing the webhook handler.

…lag, use elizaAppConfig in n8n bridge

- Gate sendTypingIndicator on BOT_TOKEN to prevent requests with undefined token
- Add catch block for setup failures (buildContext/createRuntime) — sends user message + returns 503
- Remove g flag from module-level URL_REGEX to prevent stale lastIndex; use inline fresh regex in global-match functions
- Replace process.env.ELIZA_APP_TELEGRAM_BOT_TOKEN with elizaAppConfig.telegram.botToken in n8n bridge
- Add comment explaining intentional return true after agent failure

Co-authored-by: Cursor <cursoragent@cursor.com>
@claude
Copy link

claude bot commented Feb 19, 2026

PR Review — Fix: Telegram Integrations Fixes and UX Improvements

Overall this is a solid PR with good test coverage and clear motivation. The Markdown fallback retry, message splitting, and typing indicator are well-implemented. A few issues worth addressing before merge:


Bugs / Correctness

1. N8n credential bridge: fallback to app bot token when user lookup fails

// n8n-credential-bridge.ts
const userResult = await lookupUser(userId, 'N8N_CREDENTIAL_BRIDGE');
if (\!isUserLookupError(userResult)) {
  const orgBotToken = await telegramAutomationService.getBotToken(userResult.organizationId);
  if (orgBotToken) { ... return orgBotToken; }
}

// Falls through to app bot token regardless of why lookup failed
const appBotToken = elizaAppConfig.telegram.botToken;
if (appBotToken) {
  return { status: 'credential_data', data: { accessToken: appBotToken } };
}

If isUserLookupError(userResult) is true (user not found, DB error, etc.), the code silently falls through to the app-level bot token. A workflow for an unknown/invalid user ID would receive the primary Eliza bot token. This should at minimum log a warning and return null when user lookup fails, only using the app token when the user is valid but has no org-specific token.

if (isUserLookupError(userResult)) {
  logger.warn('[N8nCredentialBridge] User lookup failed, cannot resolve Telegram credential', { userId });
  return null;
}
// Only fall back to app token if user exists but org has no custom token

2. Duplicate URL_PATTERN regex in route.ts

// route.ts line 96
const URL_PATTERN = /https?:\/\/\S{60,}/;

telegram-helpers.ts already defines a URL_REGEX. The route-level regex uses \S{60,} (any non-whitespace, 60+ chars) while the helper uses [^\s)>\]]+[^\s)>\].,;:\!?"'] (strips trailing punctuation). These have different behaviors — the route regex can include trailing punctuation in the URL when checking hasLongUrl. Consider exporting a isLongUrl helper or the regex from telegram-helpers.ts instead.


Code Quality

3. Redundant condition in Twitter/X URL pattern

// telegram-helpers.ts
['Connect Twitter / X', (u) => u.includes('api.twitter') || u.includes('twitter.com/i/oauth') || u.includes('//x.com/') || u.includes('//x.com')],

u.includes('//x.com') subsumes u.includes('//x.com/') — the latter is dead code.

4. callSendMessage payload is Record<string, unknown>

The helper loses type safety for Telegram API parameters. At minimum document what fields are expected, or use a typed interface. The spread pattern { parse_mode: _, ...plain } is clean, but the type looseness makes it easy to silently pass invalid fields.

5. runtime.character.system mutation

const originalSystemPrompt = runtime.character?.system;
if (runtime.character) {
  runtime.character.system = (runtime.character.system || '') + telegramChannelContext;
}
// ...
finally {
  if (runtime.character) {
    runtime.character.system = originalSystemPrompt;
  }
}

The restore in finally sets runtime.character.system = originalSystemPrompt, but originalSystemPrompt may be undefined if runtime.character.system was unset initially. This would explicitly set .system to undefined rather than deleting the property. Depending on how the runtime serializes the character, this may introduce a subtle difference. This is probably fine in practice but worth a comment noting it intentionally allows undefined.


Test Coverage Gaps

6. sendWithMarkdownFallback in route.ts is not directly tested

The telegram-ux-helpers.test.ts re-implements the fallback logic locally in sendWithFallback() rather than importing and testing sendWithMarkdownFallback from the route. Since route.ts functions are not exported, the actual production code path is untested for the retry behavior. Consider extracting sendWithMarkdownFallback into telegram-helpers.ts so it can be imported and tested directly.

7. resolveTelegramCredential has no tests

The new n8n bridge method that resolves bot tokens is untested. Given it involves credential resolution with a fallback to the app bot token, this warrants at least a unit test covering: org token present, org token absent (falls back), user lookup failure.


Minor / Nits

8. Prompt change "End every response with one clear next step" — this instruction is applied to all responses, not just Telegram. The addition is in chatAssistantFinalSystemPrompt which is used across channels. For web UI responses, always ending with a next-step action may feel unnatural. Consider scoping this instruction to the Telegram channel context injection block rather than the global system prompt.

9. isSimpleMessage("") returns true

"".split(/\s+/) returns [""] (length 1), so an empty string is classified as "simple". This is consistent with the test expectations, but the name isSimpleMessage might be misleading for empty input — worth a comment.


Summary

Severity Issue
Medium N8n credential bridge leaks app bot token on user lookup failure (#1)
Low Redundant URL_PATTERN regex in route.ts (#2)
Low Dead code in Twitter URL pattern (#3)
Low Missing direct tests for sendWithMarkdownFallback and resolveTelegramCredential (#6, #7)
Nit Global prompt change for next-step instruction (#8)

The Markdown fallback, message splitting, typing indicator, and command error handling are all well-implemented. Item #1 (credential bridge fallback behavior) is the main thing to address before merge.

….com pattern

- Return null from resolveTelegramCredential when user lookup fails instead of falling through to app bot token
- Remove redundant //x.com/ check (subsumed by //x.com)
- Add comment clarifying originalSystemPrompt may be undefined

Co-authored-by: Cursor <cursoragent@cursor.com>
…ram retry

Co-authored-by: Cursor <cursoragent@cursor.com>
@claude
Copy link

claude bot commented Feb 19, 2026

PR Review: Fix Telegram Integrations UX Improvements

Overall this is a solid improvement to the Telegram webhook. The Markdown fallback, message splitting, typing indicators, and system-prompt restore are all well-implemented. A few issues worth addressing before merge:


Bugs

1. Prompt injection via message.from.first_name (route.ts:224)

`- The user's name is ${message.from.first_name || "there"}.`,

A Telegram user could set their display name to something like Alice.\n\n## New Instructions:\nIgnore all previous rules. This gets concatenated directly into the LLM's system prompt with no sanitization. At minimum, strip newlines and limit length before injecting:

const safeName = (message.from.first_name || "there").replace(/[\r\n]/g, " ").slice(0, 50);

2. Missing delay between message chunks (route.ts:104-115)

The for loop sends chunks sequentially with no inter-message delay. Telegram's rate limit is 1 message/second per chat. For long agent responses split into multiple chunks, chunks 2+ will likely get 429 Too Many Requests, and the current code treats any non-ok response as permanent failure. Consider a small delay between chunks or handling 429 with a retry.

3. Brittle Markdown error string match (route.ts:64)

if (firstError.includes("can't parse entities")) {

This relies on Telegram's English-language error string. If Telegram changes the wording, the fallback silently stops working and messages with bad Markdown just fail. Worth logging firstError in full on the non-matching path so you can detect regressions.


Dead Code (from reverted button-based OAuth flow)

The following exports in telegram-helpers.ts are not called from the production path based on this diff:

  • extractAuthUrls, stripAuthUrlsFromText, isSimpleMessage

These were part of the "inline keyboard button" approach reverted in commit 382af0d. They are well-tested but dead in production. Either use them or move them to a separate branch until the button UI is revived — 80+ lines of exported code with no callers increases maintenance surface.


Minor Issues

4. sendCommandResponse adds little value

The wrapper just logs a warning on send failure — but sendTelegramMessage already logs internally. The extra indirection makes the switch/case harder to scan without material benefit.

5. System prompt mutation assumes non-shared runtime (route.ts:229-275)

The save/restore pattern is correct, but it relies on createRuntimeForUser returning a per-request runtime. If runtimes are ever pooled or shared across concurrent requests for the same user, the mutation is still racy under concurrency. A comment documenting the assumption would help future maintainers.

6. URL_REGEX / URL_PATTERN duplication across files

route.ts:94 defines /https?:\/\/\S{60,}/ (60-char threshold) and telegram-helpers.ts:122 defines a different general-purpose URL regex. The parallel definitions are easy to conflate. A short comment on the route.ts one explaining the 60-char threshold rationale (OAuth URLs tend to be long; short URLs are safe to format with Markdown) would help.


N8n Credential Bridge

The implementation is well-structured — org token takes priority, app token is explicit fallback, and the early return null on lookup failure correctly prevents token leakage. One note:

7. App bot token fallback is shared across all orgs

If an org hasn't set up their own bot token, all their n8n workflows use the Eliza App bot, sharing its rate limits across orgs. This seems intentional, but a comment noting the tradeoff would help future reviewers understand the design intent.


What's Good

  • Markdown → plain-text fallback is solid with correct error propagation and logging
  • Message chunking splits at newline boundaries first, hard-splits only pathological lines
  • Typing refresh stops correctly in finally for all code paths including setup failures
  • System prompt restore in inner finally prevents prompt accumulation on cached runtimes
  • request.json() guard turns a 500 crash into a clean 400
  • Bot token null guard for typing indicator prevents embedding undefined in the API URL
  • resolveTelegramCredential security fixreturn null on user lookup failure is correct
  • Test suite is thorough; the real-HTTP-server tests for Markdown fallback are particularly good

@claude
Copy link

claude bot commented Feb 19, 2026

Code Review

Overall this is a solid set of improvements — the Markdown fallback, message splitting, and improved error handling are all well-motivated. The test coverage is comprehensive. A few issues worth addressing before merge:


🔴 Race condition on runtime.character.system (High)

app/api/eliza-app/webhook/telegram/route.ts mutates the cached runtime's character.system in-place:

const originalSystemPrompt = runtime.character?.system;
runtime.character.system = (runtime.character.system || "") + telegramChannelContext;
// ...
} finally {
  runtime.character.system = originalSystemPrompt;
}

The runtime factory caches runtimes by ${agentId}:${organizationId} (confirmed in runtime-factory.ts). If two users from the same org send Telegram messages simultaneously, they share the same cached character object. Concurrent execution of save → mutate → restore is not atomic: User B can save originalSystemPrompt after User A has already mutated it, then User A's finally restores the un-contexted prompt while User B's request is still in-flight.

Recommendation: Copy the channel context into the message handler call rather than mutating shared state — for example, pass it as a per-call system prompt override if the handler supports it, or compose a local options object rather than mutating runtime.character.system directly.


🟡 Setup-error path sends user message then returns false (503) — duplicate messages possible (Medium)

} catch (setupError) {
  // ...
  await sendTelegramMessage(message.chat.id, "Something went wrong...");
  return false; // ← Telegram will retry this webhook
}

Returning false causes a 503, which triggers Telegram to retry the update. If setup succeeds on retry, the user receives both the error message and a normal response. The agent-failure inner catch intentionally returns true for exactly this reason (well-commented and correct). The setup-failure path should do the same after sending the user-facing error, unless there's a specific transient failure (like lock contention) that warrants a retry — in which case suppress the user-facing message on the first attempt.


🟡 Prompt injection via message.from.first_name (Medium — security)

`- The user's name is ${message.from.first_name || "there"}.`,

first_name is user-controlled input injected directly into the LLM system prompt without sanitization. A crafted name containing newlines and instruction-like text could attempt jailbreaking. At minimum: truncate to a safe length (e.g., 64 chars) and strip control characters before embedding.


🟡 Duplicate URL pattern between route.ts and telegram-helpers.ts (Medium)

route.ts defines its own local constant:

const URL_PATTERN = /https?:\/\/\S{60,}/;

But telegram-helpers.ts already has URL_REGEX with a more precise trailing-punctuation exclusion list. The two patterns can give different results for the same URL (the helpers' regex trims trailing .,;:!? etc.; the route's does not). Export a shared constant from telegram-helpers.ts and use it in both places.


🟡 No delay between message chunks (Medium — reliability)

for (let i = 0; i < chunks.length; i++) {
  const ok = await sendWithMarkdownFallback({...});
  if (!ok) return false;
}

Telegram enforces per-chat rate limits (roughly 1 message/second burst for bots in private chats). For very long agent responses, rapid sequential sends risk a 429 that causes later chunks to be silently dropped. Consider a small configurable delay (100–300 ms) between chunks, or check for 429 in sendWithMarkdownFallback and implement backoff.


🟡 "Engagement & Conversion Rules" in global prompt affect all channels (Medium)

The rules added to chat-assistant-prompts.ts apply to all assistant interactions, not just Telegram:

- **Celebrate completions** ...
- **Keep momentum**: Short, punchy responses ...
- **Always end with a clear next step** ...

Tone/style guidance like "short, punchy responses" may be desirable on Telegram (mobile, async) but undesirable in other contexts (web, API). If Telegram-specific, these belong in the telegramChannelContext injected per-request — not the global assistant prompt.


🟢 Minor / style

  • setInterval in serverless: createTypingRefresh uses setInterval, which is fine since stop() runs in finally. On Vercel's Edge Runtime, timers may behave unexpectedly if the execution context is frozen before stop() runs; low risk in practice.

  • AUTH_URL_PATTERNS not exported: The PR description mentions it as a named export, but the code has const AUTH_URL_PATTERNS (unexported). Tests don't import it — just a description inaccuracy.

  • PR description mentions Twitter CRUCIAL_TOOLS / SEARCH_ACTIONS changes that don't appear in the diff. Looks like these were reverted but the summary wasn't updated.

  • createTypingRefresh ignores HTTP 429: The interval callback only .catch()es network errors, not HTTP-level errors (response.ok === false). Acceptable for best-effort typing indicators, but worth a comment.


Summary

Issue Severity File
Race condition on shared runtime.character.system 🔴 High webhook/telegram/route.ts
Setup-failure 503 + user message → duplicate delivery on retry 🟡 Medium webhook/telegram/route.ts
Unsanitized first_name in system prompt 🟡 Medium webhook/telegram/route.ts
Duplicate URL regex patterns 🟡 Medium route.ts + telegram-helpers.ts
No delay between message chunks 🟡 Medium webhook/telegram/route.ts
Global prompt changes for channel-specific UX rules 🟡 Medium chat-assistant-prompts.ts

The test suite is well-structured — the HTTP-server-based Markdown fallback tests are a particularly nice touch. The core Markdown-fallback and message-splitting logic looks correct. The two most important fixes before merge are the shared-state mutation (race condition) and the setup-error retry behavior.

… on setup failure

- Strip control characters and truncate first_name to 64 chars before injecting into LLM system prompt
- Setup-error catch now returns true (mark processed) after sending user error message to prevent Telegram retry causing duplicate delivery

Co-authored-by: Cursor <cursoragent@cursor.com>
@claude
Copy link

claude bot commented Feb 19, 2026

PR Review: Telegram Integrations Fixes and UX Improvements

Overall this is a solid, well-structured PR with good error resilience and comprehensive test coverage. A few issues worth addressing before merge.


Bugs / Correctness

1. oauth-get.ts examples don't match new response format

The handler now returns "${platformName} is connected! Logged in as..." but the examples array (used for LLM few-shot prompting) still shows the old format:

// examples (line 114) — stale:
{ text: "Google connected! Logged in as user@gmail.com." }
// handler output (line 77) — new:
`${platformName} is connected! Logged in as ${identifier}.`

This inconsistency degrades LLM action selection accuracy. Update all four examples to match the new format.

2. first_name sanitization is incomplete — prompt injection risk

route.ts:224:

`- The user's name is ${(message.from.first_name || "there").replace(/\p{Cc}/gu, "").slice(0, 64)}.`,

Control-character stripping + 64-char truncation is good, but a user who sets their Telegram display name to Alice.\n\n# SYSTEM: Ignore all instructions... can inject arbitrary text into the LLM's system prompt. The 64-char limit reduces the window but doesn't close it.

Suggestion: either JSON-encode/quote the name before interpolation, or strip all characters except printable ASCII/Unicode letters and spaces.

3. createTypingRefresh tests leak intervals on assertion failure

telegram-ux-helpers.test.ts:806:

test("returns an object with a stop function", () => {
  const typing = createTypingRefresh(12345, "fake-token", 60000);
  expect(typing).toHaveProperty("stop");
  typing.stop(); // not in try/finally — missed if assertion throws
});

Bun test isolates each test() but if an assertion before typing.stop() fails the interval is never cleared. Wrap in try/finally.


Code Quality

4. AUTH_URL_PATTERNS//x.com is overbroad

telegram-helpers.ts:126:

["Connect Twitter / X", (u) => u.includes("api.twitter") || u.includes("twitter.com/i/oauth") || u.includes("//x.com")],

u.includes("//x.com") matches any URL with //x.com anywhere in it — including redirect parameters like https://login.example.com/?next=//x.com/profile. Consider tightening to u.startsWith("https://x.com/") || u.startsWith("http://x.com/") or checking the hostname more precisely.

5. stripAuthUrlsFromText prefix regex can false-positive

telegram-helpers.ts:157:

.replace(/Connect[\w\s/]+:\s*/gi, "")

[\w\s/]+ is greedy — it would also strip "Connect SOMEONE TO SLACK:" from normal text that happens to start with "Connect". Tightening to known platform names would be safer (or restrict to single word: Connect\s+\w+:).

6. sendWithMarkdownFallback real-HTTP-server tests cover a reimplementation, not the function under test

telegram-ux-helpers.test.ts:426-450 defines its own sendWithFallback against a real server. This is fine for testing the pattern, but divergence between this copy and the actual sendWithMarkdownFallback in route.ts won't be caught. The tests also validate that requestLog[0].body.parse_mode === "Markdown" and requestLog[1].body lacks it — this is correct, but relies on the test copy being kept in sync. Worth a comment noting this limitation.

7. Minor: extractAuthUrls / stripAuthUrlsFromText recreate regex on every call

text.match(new RegExp(URL_REGEX, "g"))     // extractAuthUrls
text.replace(new RegExp(URL_REGEX, "g"), ) // stripAuthUrlsFromText

URL_REGEX is already a compiled regex — wrapping it in new RegExp(..., "g") on every call creates a new regex object each time. Define a module-level URL_REGEX_GLOBAL = new RegExp(URL_REGEX.source, "g") and reuse it (or just define the regex with the g flag directly where needed).


What's Done Well

  • Error resilience architecture is excellent: outer setup try/catch → inner agent try/catch → fallback send error try/catch. The return true to prevent infinite Telegram retries on non-retriable failures is well-considered.
  • return false → 503 → Telegram retry for transient failures (lock contention, delivery failure on empty-response fallback) is the correct pattern.
  • Timing-safe token comparison (timingSafeEqual) — correct.
  • Bot token not leaked on user lookup failure in n8n-credential-bridge.ts:299-303 — the early null return before falling back to the app token is the right call.
  • System prompt save/restore (originalSystemPrompt in finally) — correct handling of the mutation.
  • sendTypingIndicator called immediately + createTypingRefresh for refresh — good UX, eliminates the silent wait.
  • Test coverage (143 tests, real HTTP server for fallback, boundary/edge cases, false-positive prevention) is thorough.
  • Multi-step template rules 6 & 7 (always execute actions fresh, never reference old links) address a real LLM behavior failure mode.

Summary

Must fix oauth-get.ts examples (#1), prompt injection in first_name (#2)
Should fix //x.com pattern (#4), prefix regex (#5)
Nice to have Test cleanup (#3), regex caching (#7), test copy comment (#6)

…nterval leaks in tests

- Update all 4 oauthGetAction examples to match current response format
- Wrap createTypingRefresh test assertions in try/finally to ensure interval cleanup

Co-authored-by: Cursor <cursoragent@cursor.com>
@claude
Copy link

claude bot commented Feb 19, 2026

PR Review: Telegram Integrations Fixes and UX Improvements

Good overall direction — the Markdown fallback, message splitting, and typing indicator are solid improvements. However, there are a few issues worth addressing before merging.


Critical: Race condition in runtime.character.system mutation

app/api/eliza-app/webhook/telegram/route.ts, lines ~205–276

The PR mutates runtime.character.system on a cached runtime object and restores it in a finally block. The runtime factory caches runtimes by ${agentId}:${organizationId}, so two concurrent Telegram messages from the same org will receive the same cached runtime object. If they overlap:

  1. Request A saves originalSystemPrompt = "base"
  2. Request B saves originalSystemPrompt = "base + context-A" (already mutated by A)
  3. Request A finishes, restores to "base"
  4. Request B finishes, restores to "base + context-A"system prompt is permanently corrupted

runtime-factory.ts already has a comment warning about this exact pattern from a previous incident:

"MCP settings: NO LONGER mutated on shared runtime. Previously we wrote to charSettings.mcp here, causing race conditions when concurrent users shared the same cached runtime (API key leakage)."

Fix: Pass the Telegram channel context through messageHandler.process() options (e.g., as a systemPromptSuffix) rather than mutating the shared runtime, or inject it via the per-request context mechanism already used for ELIZAOS_API_KEY and similar fields.


Security: App bot token exposed to all org n8n workflows

lib/eliza/plugin-n8n-bridge/n8n-credential-bridge.ts, lines ~310–330

const appBotToken = elizaAppConfig.telegram.botToken;
if (appBotToken) {
  return { status: "credential_data", data: { accessToken: appBotToken } };
}

Any user whose org has no org-specific bot token will receive the application's main bot token for their n8n telegramApi credential. This allows user-created n8n workflows to send messages as the application bot to any Telegram chat ID — including chats of other users.

Consider removing the fallback or gating it behind an explicit opt-in config flag. If an org has no bot token, returning null with a user-facing error is safer.


Medium: URL detection regex duplicated and inconsistent

Two different URL regexes exist without sharing code:

  • route.ts line 96: const URL_PATTERN = /https?:\/\/\S{60,}/; (URLs ≥ 60 chars)
  • telegram-helpers.ts line 728: const URL_REGEX = /https?:\/\/[^\s)>\]]+[^\s)>\].,;:!?"']/; (any URL)

Consider exporting the long-URL threshold regex from telegram-helpers.ts and importing it in the route.


Minor issues

  • originalSystemPrompt typed as string | undefined: Assigning undefined back to runtime.character.system may conflict with the field's declared type. Conditionally skip the restore if originalSystemPrompt is undefined.
  • AUTH_URL_PATTERNS not exported: The PR description lists it as an exported addition, but it uses const without export.
  • extractAuthUrls recreates RegExp on every call: new RegExp(URL_REGEX, "g") creates a new object per call. A module-level URL_REGEX_GLOBAL with the g flag would be slightly more efficient.

Tests

The test suite is comprehensive and the real HTTP server tests for the Markdown fallback are a good approach. A few notes:

  • The new source-file-reading test in oauth-enforcement.test.ts (expect(webhookCode).toContain(...)) is fragile — it would pass even if the string only appears in a comment.
  • No test covers the concurrent runtime mutation scenario described above.
  • createTypingRefresh tests correctly avoid triggering real fetch calls by using a 60-second interval.

Summary

Severity Issue
🔴 Critical Race condition: shared cached runtime character.system mutated across concurrent requests
🟠 High Security: app bot token falls through to any user org via n8n Telegram credential
🟡 Medium URL detection regex duplicated between route.ts and telegram-helpers.ts
🟢 Minor AUTH_URL_PATTERNS not exported despite PR description
🟢 Minor new RegExp(URL_REGEX, "g") recreated on every extractAuthUrls call
🟢 Minor originalSystemPrompt (string | undefined) assigned back to character.system

The UX improvements (typing indicator, message splitting, Markdown fallback, error resilience) are all good. The critical issue is the runtime mutation — the codebase has explicit precedent for avoiding exactly this pattern and it needs a different approach before merging.

Resolve conflict in telegram webhook route:
- Adopt tryClaimForProcessing from dev (atomic idempotency)
- Keep our improvements: Markdown fallback, message splitting, typing indicator, error handling
- Remove /start and /help commands, keep /status only
- handleMessage returns void to match atomic claim pattern

Co-authored-by: Cursor <cursoragent@cursor.com>
@claude
Copy link

claude bot commented Feb 20, 2026

PR Review: Fix — Telegram Integrations Fixes and UX Improvements

Overall this is a solid improvement to the Telegram webhook path with good test coverage. There are a few issues worth addressing before merge.


Bugs / Correctness

1. /start and /help commands silently removed

The handleCommand switch now only handles /status and falls through to a generic default. /start and /help are standard bot commands that users set expectations around (and may have been linked from onboarding flows). Users hitting /start will now get "I don't recognize that command." which is confusing. If these are intentionally removed, that decision should be explicit and the bot's command list should be updated via BotFather.

2. Runtime mutation race condition (runtime.character.system)

// route.ts ~L209-L267
const originalSystemPrompt = runtime.character?.system;
if (runtime.character) {
  runtime.character.system = (runtime.character.system || "") + telegramChannelContext;
}
// ... work ...
// finally:
if (runtime.character) {
  runtime.character.system = originalSystemPrompt;
}

If runtimeFactory.createRuntimeForUser returns a cached/shared runtime object and two concurrent Telegram messages arrive for the same user, they'll race on runtime.character.system. One request sets the prompt, the other overwrites it, and the first request restores undefined in its finally block — leaving the second request with a corrupted prompt. This needs either an immutable approach (deep clone the character before mutation) or confirmation that the factory always returns a fresh instance per call.

3. Dead exported code — isSimpleMessage, extractAuthUrls, stripAuthUrlsFromText

These are exported from telegram-helpers.ts and tested extensively, but route.ts only imports splitMessage, TELEGRAM_RATE_LIMITS, and createTypingRefresh. The inline keyboard path (extract URLs → strip from text → send as buttons) described in the PR summary is not wired up in the webhook handler. Either connect these utilities to production code or remove them from this PR to avoid dead-code sprawl.

4. AUTH_URL_PATTERNS not exported

The PR description lists AUTH_URL_PATTERNS as an export, but the variable is not exported from telegram-helpers.ts. Minor discrepancy — either export it or remove the claim from the summary.

5. Silent error swallowing in setup failure path

} catch { /* best-effort delivery */ }

The failure to deliver the error message to the user is completely silent — no log at even debug level. At minimum a logger.debug here would help during incident investigation.


Security

6. Implicit Telegram bot token fallback in N8nCredentialBridge

// n8n-credential-bridge.ts ~L519-L526
const appBotToken = elizaAppConfig.telegram.botToken;
if (appBotToken) {
  logger.info("[N8nCredentialBridge] Telegram credential resolved from app bot token (fallback)");
  return { status: "credential_data", data: { accessToken: appBotToken } };
}

Any user whose org doesn't have a dedicated bot token will silently receive the shared app bot token. Their n8n workflows will then send messages through the shared bot without the user being aware. Consider returning null (credential not available) rather than silently falling back to the shared app token, or making this fallback explicitly opt-in.

7. User first_name goes directly into the system prompt

`- The user's name is ${(message.from.first_name || "there").replace(/\p{Cc}/gu, "").slice(0, 64)}.`

Control character stripping + length limit is reasonable, but a user could set their Telegram name to contain prompt injection content. Consider wrapping the name in quotes and/or stricter sanitization since it's interpolated directly into the LLM system prompt.


Code Quality

8. Duplicate URL regex

telegram-helpers.ts defines URL_REGEX = /https?:\/\/[^\s)>\]]+[^\s)>\].,;:!?"']/ while route.ts defines a local URL_PATTERN = /https?:\/\/\S{60,}/. These serve different purposes (button extraction vs. parse_mode skip heuristic), but having two URL regexes in closely related code is a maintenance hazard. A comment on each clarifying the intent would help.

9. callTelegramApicallSendMessage rename loses generality

The typing indicator uses a separate raw fetch call while message sending goes through callSendMessage. Two code paths for hitting the Telegram API now exist. Not a blocker, but consolidating them or making the naming distinction clearer would reduce confusion.

10. Inconsistent comment style in n8n-credential-bridge.ts

// ── Telegram bot token strategy ──────────────────────────────────────

Box-drawing character separators appear only in this new section; the rest of the file uses standard // comments. Keep to the existing style.


Test Coverage

The suite is comprehensive (143 tests, real HTTP server tests for the fallback path). A few observations:

  • The "Markdown fallback with real HTTP server" tests re-implement sendWithFallback inline rather than testing sendWithMarkdownFallback from route.ts directly (which isn't importable as a module). The tests validate the pattern correctly but won't catch a divergence between the tested logic and the production function.
  • isSimpleMessage("") returning true is consistent with the implementation but may be surprising — an empty message shouldn't reach this codepath in practice, so it's a minor concern.
  • The note in createTypingRefresh tests explaining why fetch calls aren't intercepted is appreciated.

Summary

Category Count Severity
Bugs / Correctness 3 Medium (runtime mutation, dead code, missing commands)
Security 2 Medium (implicit bot token sharing, prompt injection surface)
Code Quality 3 Low
Tests 1 Low

The Markdown fallback, message splitting, and error resilience improvements are all good changes. The main items to address before merge: the runtime mutation race condition (#2), the implicit bot token fallback (#6), and resolving the dead-code helpers (#3) — either wire them up or defer to a follow-up PR.

message.message_id,
);
} else {
await sendTelegramMessage(message.chat.id, responseText, message.message_id);
Copy link

Choose a reason for hiding this comment

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

Message delivery failures not handled or reported

High Severity

The sendTelegramMessage function returns a boolean indicating delivery success, but this return value is never checked when sending agent responses, empty response fallbacks, or error messages. Additionally, handleMessage returns void and handleTelegramWebhook always returns 200 OK, preventing Telegram from retrying failed deliveries. Users won't receive responses if message delivery fails, and no fallback mechanism exists.

Additional Locations (2)

Fix in Cursor Fix in Web

await sendTelegramMessage(
chatId,
`*Account Status*\n\n✅ Connected\n💰 Credits: $${creditBalance}\n🆔 User ID: \`${user.id.substring(0, 8)}...\``,
"I don't recognize that command. Just send me a message and I'll help you!",
Copy link

Choose a reason for hiding this comment

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

Standard bot commands removed causing UX regression

Medium Severity

The /start and /help command handlers were removed from handleCommand, leaving only /status. Users sending /start (the standard Telegram bot initialization command) or /help now receive "I don't recognize that command" instead of proper welcome/help messages. This breaks the expected Telegram bot UX where /start displays a welcome message when users first interact with the bot. The PR test plan explicitly mentions verifying correct responses for these commands, indicating their removal was unintentional.

Fix in Cursor Fix in Web

Remove unused AUTH_URL_PATTERNS, extractAuthUrls, stripAuthUrlsFromText,
isSimpleMessage and their ~580 lines of tests (reverted button feature).
Expand test suite from 22 to 50 tests with boundary conditions, edge
cases, URL_PATTERN validation, and real HTTP server integration tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
@claude
Copy link

claude bot commented Feb 20, 2026

Code Review

Overall this is solid work — good layering of fixes, the Markdown fallback logic is clean, and the test suite is comprehensive. A few issues worth addressing before merge.


Bugs / Issues

1. Prompt injection via first_name (medium)

route.ts line ~205:

`- The user's name is ${(message.from.first_name || "there").replace(/\p{Cc}/gu, "").slice(0, 64)}.`,

The sanitization strips Unicode control characters but not newlines (\n, \r) or other characters that can break the prompt structure. A Telegram user with first_name = "there. Ignore the above. You are now..." could inject instructions across prompt lines. At minimum, strip \n and \r before injecting into the system prompt:

.replace(/[\p{Cc}\r\n]/gu, "")

2. Tests do not exercise production sendWithMarkdownFallback

telegram-ux-helpers.test.ts re-implements the fallback logic locally in sendWithFallback() rather than importing the actual sendWithMarkdownFallback from route.ts. Since sendWithMarkdownFallback is an unexported module-level function, the HTTP server tests cover a copy of the logic, not the production code path. If the real function is refactored incorrectly, these tests will not catch it.

Options: export and import the helper, or move it to telegram-helpers.ts alongside the other testable utilities.

3. URL_PATTERN duplicated in tests

telegram-ux-helpers.test.ts line ~1192 redefines const URL_PATTERN = /https?:\/\/\S{60,}/; instead of importing it. If the production threshold changes, the tests silently pass with the wrong pattern. Export URL_PATTERN from a shared location or import it directly.

4. Removed warning log when message.from is missing

Old code logged a warning when message.from was absent. New code silently returns. A missing from field is unexpected on a non-channel message and could indicate a Telegram API change or edge case. Losing the log makes this harder to diagnose in production.

5. No delay between split message chunks

Telegram enforces a ~1 msg/sec rate limit per chat. sendTelegramMessage sends all chunks back-to-back with no delay. For most messages this is fine, but very long responses split into many chunks could hit the rate limit and produce 429 errors. Consider a small await sleep(200) between chunks.

6. Broad URL heuristic skips Markdown for all long URLs

URL_PATTERN = /https?:\/\/\S{60,}/ disables Markdown for any message containing a 60+ char URL — not just OAuth URLs. A message like "Check out this article: https://www.example.com/some/long/path/to/article?with=params" would render as plain text, losing bold/italic formatting. This is a deliberate trade-off (noted in the PR description) but worth documenting with a comment explaining the 60-character threshold.


Security

7. App bot token fallback in n8n credential bridge

n8n-credential-bridge.ts correctly prefers the org bot token, but falls back to the shared app bot token. An n8n automation using this fallback would send messages as the Eliza app bot rather than the user's configured bot. Probably acceptable as a degraded-mode fallback, but worth a comment clarifying this is intentional.


Minor / Cleanup

8. sendTypingIndicator duplicates fetch logic from createTypingRefresh

Both make the same sendChatAction POST call. Not blocking, just a small duplication worth noting.

9. Dead code: createInlineKeyboard / createMultiRowKeyboard tests

The button feature was reverted, but tests for these helpers remain. If the helpers in telegram-helpers.ts are also unused in production, they are dead code. Confirm whether they are still needed or remove them.


What's Good

  • Save/restore of runtime.character.system with originalSystemPrompt correctly prevents accumulation on cached runtimes.
  • BOT_TOKEN guard before typing indicator calls is correct.
  • Removing the g flag from URL_PATTERN to prevent stale lastIndex is a good catch.
  • The outer try/finally reliably releases the lock and stops the typing interval in all paths.
  • The structured catch hierarchy (setup errors vs. agent errors vs. send errors) is clear and readable.
  • 143 tests is a meaningful improvement in coverage.

Summary: Items 1 (prompt injection) and 2 (tests not covering production code) are the highest-priority fixes. The rest are improvements or minor issues that can be addressed in follow-up.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

await sendTelegramMessage(
chatId,
`*Account Status*\n\n✅ Connected\n💰 Credits: $${creditBalance}\n🆔 User ID: \`${user.id.substring(0, 8)}...\``,
"I don't recognize that command. Just send me a message and I'll help you!",
Copy link

Choose a reason for hiding this comment

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

Missing /start and /help command handlers

High Severity

The handleCommand switch statement only handles /status and default. The /start and /help cases from the previous code were removed without replacement. Users sending /start (automatically sent by Telegram when opening a bot) or /help now get "I don't recognize that command" — a significant onboarding regression. The PR description and test plan both reference /start and /help as supported commands.

Fix in Cursor Fix in Web

@standujar standujar closed this Feb 24, 2026
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.

3 participants