Fix : Telegram Integrations Fixes and UX improvements#359
Fix : Telegram Integrations Fixes and UX improvements#359
Conversation
- 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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Use the checkbox below for a quick retry:
Note 🎁 Summarized by CodeRabbit FreeYour 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 |
…fix/telegram-ux-overhaul
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: Bugs1. ["Connect Twitter / X", (u) => u.includes("api.twitter") || u.includes("twitter.com/i/oauth") || u.includes("x.com")],
Fix: tighten to 2. 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 ( Fix: add word boundaries — 3. Mutable runtime character mutation ( runtime.character.system = (runtime.character.system || "") + telegramChannelContext;If Either deep-clone the character before mutation, inject the context as a separate system message, or confirm 4. 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 Security5. App bot token shared across all orgs ( 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. Minor6. .replace(/Connect \w+:\s*/gi, "")
Fix: What's Good
|
There was a problem hiding this comment.
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.
lib/utils/telegram-helpers.ts
Outdated
| const URL_REGEX = /https?:\/\/[^\s)>\]]+/g; | ||
|
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
lib/utils/telegram-helpers.ts
Outdated
| 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") |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| 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 }]), | ||
| }, | ||
| }); |
There was a problem hiding this comment.
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).
| 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; |
There was a problem hiding this comment.
N/A — sendTelegramMessageWithButtons was removed entirely during the feature revert. All messages now go through sendTelegramMessage which uses splitMessage() for chunking.
| 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); | ||
| } |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
Fixed — sendTelegramMessage return value is now checked. If the final response fails to deliver, we return false to trigger a 503 and Telegram retry.
| 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"] } }, | ||
| ], |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Fixed — examples updated to match the current handler output format (Here's your [Platform] authorization link...).
| 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 }, | ||
| }; | ||
| } |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
| test("accepts onError callback without crashing", async () => { | ||
| const typing = createTypingRefresh(12345, "fake-token", 50, () => {}); | ||
| await new Promise((r) => setTimeout(r, 120)); | ||
| typing.stop(); | ||
| }); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Fixed — intervalMs increased to 60000ms so the typing refresh never fires during the test.
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 RuntimesFile: if (runtime.character) {
runtime.character.system = (runtime.character.system || "") + telegramChannelContext;
}
Every other handler in this codebase (see 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 🟠 Security: App Bot Token Returned to All Users Without ScopingFile: const appBotToken = process.env.ELIZA_APP_TELEGRAM_BOT_TOKEN;
if (appBotToken) {
return { status: "credential_data", data: { accessToken: appBotToken } };
}
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 🟡 Tests Test Reproduced Logic, Not Production FunctionsFile: Three test suites don't test the production code path:
Since 🟡
|
| 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.
… 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>
Code ReviewOverall 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
if (runtime.character) {
runtime.character.system = (runtime.character.system || "") + telegramChannelContext;
}This mutates 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:
|
| 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 |
There was a problem hiding this comment.
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.
|
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 Extracting it to 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>
PR ReviewGood 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. CriticalSystem prompt mutation is not concurrency-safe 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 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 Medium
// createTypingRefresh is gated...
typing = BOT_TOKEN ? createTypingRefresh(...) : null;
// ...but sendTypingIndicator is not
await sendTypingIndicator(message.chat.id); // calls with undefined tokenURL length threshold (60 chars) is fragile const URL_PATTERN = /https?:\/\/\S{60,}/;A short OAuth URL like Module-level global regex with const URL_REGEX = /https?:\/\/[^\s)>\]]+[^\s)>\].,;:\!?"']/g;A module-level regex with the
.replace(/Connect[\w\s\/]+:\s*/gi, "")This removes any text matching MinorExplanatory comments removed without replacement
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
Prompt: "End every response with one clear next step"
Positives
|
…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>
PR Review — Fix: Telegram Integrations Fixes and UX ImprovementsOverall 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 / Correctness1. 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 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 token2. Duplicate // route.ts line 96
const URL_PATTERN = /https?:\/\/\S{60,}/;
Code Quality3. 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')],
4. The helper loses type safety for Telegram API parameters. At minimum document what fields are expected, or use a typed interface. The spread pattern 5. 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 Test Coverage Gaps6. The 7. 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 / Nits8. Prompt change "End every response with one clear next step" — this instruction is applied to all responses, not just Telegram. The addition is in 9.
Summary
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>
PR Review: Fix Telegram Integrations UX ImprovementsOverall 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: Bugs1. Prompt injection via `- The user's name is ${message.from.first_name || "there"}.`,A Telegram user could set their display name to something like const safeName = (message.from.first_name || "there").replace(/[\r\n]/g, " ").slice(0, 50);2. Missing delay between message chunks ( The 3. Brittle Markdown error string match ( 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 Dead Code (from reverted button-based OAuth flow)The following exports in
These were part of the "inline keyboard button" approach reverted in commit Minor Issues4. The wrapper just logs a warning on send failure — but 5. System prompt mutation assumes non-shared runtime ( The save/restore pattern is correct, but it relies on 6.
N8n Credential BridgeThe implementation is well-structured — org token takes priority, app token is explicit fallback, and the early 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
|
Code ReviewOverall 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
|
| 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>
PR Review: Telegram Integrations Fixes and UX ImprovementsOverall this is a solid, well-structured PR with good error resilience and comprehensive test coverage. A few issues worth addressing before merge. Bugs / Correctness1. The handler now returns // 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.
`- 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 Suggestion: either JSON-encode/quote the name before interpolation, or strip all characters except printable ASCII/Unicode letters and spaces. 3.
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 Code Quality4.
["Connect Twitter / X", (u) => u.includes("api.twitter") || u.includes("twitter.com/i/oauth") || u.includes("//x.com")],
5.
.replace(/Connect[\w\s/]+:\s*/gi, "")
6.
7. Minor: text.match(new RegExp(URL_REGEX, "g")) // extractAuthUrls
text.replace(new RegExp(URL_REGEX, "g"), …) // stripAuthUrlsFromText
What's Done Well
Summary
|
…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>
PR Review: Telegram Integrations Fixes and UX ImprovementsGood 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
|
| 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>
PR Review: Fix — Telegram Integrations Fixes and UX ImprovementsOverall this is a solid improvement to the Telegram webhook path with good test coverage. There are a few issues worth addressing before merge. Bugs / Correctness1. The 2. Runtime mutation race condition ( // 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 3. Dead exported code — These are exported from 4. The PR description lists 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 Security6. Implicit Telegram bot token fallback in // 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 7. User `- 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 Quality8. Duplicate URL regex
9. The typing indicator uses a separate raw 10. Inconsistent comment style in // ── Telegram bot token strategy ──────────────────────────────────────Box-drawing character separators appear only in this new section; the rest of the file uses standard Test CoverageThe suite is comprehensive (143 tests, real HTTP server tests for the fallback path). A few observations:
Summary
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); |
There was a problem hiding this comment.
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)
| 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!", |
There was a problem hiding this comment.
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.
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>
Code ReviewOverall 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 / Issues1. Prompt injection via
`- 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 ( .replace(/[\p{Cc}\r\n]/gu, "")2. Tests do not exercise production
Options: export and import the helper, or move it to 3.
4. Removed warning log when Old code logged a warning when 5. No delay between split message chunks Telegram enforces a ~1 msg/sec rate limit per chat. 6. Broad URL heuristic skips Markdown for all long URLs
Security7. App bot token fallback in n8n credential bridge
Minor / Cleanup8. Both make the same 9. Dead code: The button feature was reverted, but tests for these helpers remain. If the helpers in What's Good
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. |
There was a problem hiding this comment.
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!", |
There was a problem hiding this comment.
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.


Summary
client_id,redirect_uri, etc.) broke Telegram's legacy Markdown parser.sendTelegramMessagereturned 400 "can't parse entities" and silently dropped the message. Users never received the OAuth link. Fix: messages with long URLs (60+ chars) skipparse_modeentirely, letting Telegram auto-link them. AsendWithMarkdownFallbackretry layer also catches any other Markdown failures and resends as plain text.callTelegramApiandsendWithMarkdownFallbackhelpers: Deduplicated thefetchlogic for Telegram API calls and added automatic Markdown-to-plain-text fallback on parse errors.splitMessage()to stay within Telegram's 4096-char limit instead of silently truncating.sendTypingIndicatoron message receipt andcreateTypingRefresh(periodic 4s interval) so users see "typing..." while the agent processes — eliminates the silent wait.telegramChannelContextblock 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.sendTelegramMessageis itself wrapped in try/catch. Request body parse failures return 400 instead of crashing.handleCommandis wrapped in try/catch.sendCommandResponsewrapper: Command responses (/start,/help,/status) go through a wrapper that logs delivery failures.telegram-helpers.ts): AddedextractAuthUrls,stripAuthUrlsFromText,isSimpleMessage,createTypingRefresh,AUTH_URL_PATTERNSfor OAuth URL detection across 10+ platforms.telegram-ux-helpers.test.ts(new, 836 lines) andoauth-enforcement.test.ts(updated) covering message splitting, Markdown fallback, auth URL extraction/stripping, typing refresh,isSimpleMessageheuristic, and composition tests.OAUTH_CONNECTfresh. Added response guidelines to preserve URLs and provide clear next steps.twittertoCRUCIAL_TOOLSandSEARCH_ACTIONSplatform enum so Twitter tools surface properly.resolveTelegramCredentialtoN8nCredentialBridgeso n8n workflows can use the Telegram bot token fortelegramApicredential type.Test plan
OAUTH_CONNECTfor Google/Linear/Twitter via TelegramMarkdown parse failed, retrying as plain textwarn (expected for URL messages)/start,/help,/statuscommands and verify correct responsestelegramApicredential resolvesbun test tests/unit/eliza-app/telegram-ux-helpers.test.ts tests/unit/eliza-app/oauth-enforcement.test.ts— all 143 tests passNote
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
telegramApicredential resolution inN8nCredentialBridgeusing 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.