fix: telegram ESM compatibility and improved diagnostics (#161)

- Replace telegram-markdown-v2 with telegramify-markdown (ESM compatible)
- Add raw text fallback when Telegram formatting fails, with error notice
- Improve empty response diagnostics: log agent ID, show conversation ID
- Add reset-conversation command hint to user messages
- Add telegram-format.test.ts with 7 tests

Fixes Railway deployment ERR_REQUIRE_ESM error with remark package.

Written by Cameron and Letta Code

"The best error message is the one that never shows up." - Thomas Fuchs
This commit is contained in:
Cameron
2026-02-05 10:31:53 -08:00
committed by GitHub
parent 257da79e94
commit c85c4a3272
6 changed files with 700 additions and 1133 deletions

1733
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -56,7 +56,7 @@
"open": "^11.0.0",
"openai": "^6.17.0",
"qrcode-terminal": "^0.12.0",
"telegram-markdown-v2": "^0.0.4",
"telegramify-markdown": "^1.0.0",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"update-notifier": "^7.3.1",

View File

@@ -0,0 +1,43 @@
import { describe, expect, it } from 'vitest';
import { markdownToTelegramV2 } from './telegram-format.js';
describe('markdownToTelegramV2', () => {
it('converts bold text', async () => {
const result = await markdownToTelegramV2('**hello**');
expect(result).toContain('*hello*');
});
it('converts inline code', async () => {
const result = await markdownToTelegramV2('use `npm install`');
expect(result).toContain('`npm install`');
});
it('escapes special characters outside formatting', async () => {
const result = await markdownToTelegramV2('Hello! How are you?');
expect(result).toContain('\\!');
});
it('handles code blocks', async () => {
const result = await markdownToTelegramV2('```js\nconsole.log("hi")\n```');
expect(result).toContain('```');
});
it('returns something for any input (never throws)', async () => {
// Even weird inputs should return without throwing
const weirdInputs = ['', '\\', '[](){}', '****', '```'];
for (const input of weirdInputs) {
const result = await markdownToTelegramV2(input);
expect(typeof result).toBe('string');
}
});
it('handles links', async () => {
const result = await markdownToTelegramV2('Check out [Google](https://google.com)');
expect(result).toContain('https://google.com');
});
it('preserves plain text', async () => {
const result = await markdownToTelegramV2('Just some plain text');
expect(result).toContain('Just some plain text');
});
});

View File

@@ -11,13 +11,13 @@
*/
export async function markdownToTelegramV2(markdown: string): Promise<string> {
try {
// Dynamic import to avoid ESM/CommonJS compatibility issues
const { convert } = await import('telegram-markdown-v2');
// Use 'keep' strategy to preserve blockquotes (>) and other elements
return convert(markdown, 'keep');
// Dynamic import to handle ESM module
const telegramifyMarkdown = (await import('telegramify-markdown')).default;
// Use 'keep' strategy to preserve blockquotes (>) and other unsupported elements
return telegramifyMarkdown(markdown, 'keep');
} catch (e) {
console.error('[Telegram] Markdown conversion failed, using fallback:', e);
// Fallback: escape special characters manually
console.error('[Telegram] Markdown conversion failed, using escape fallback:', e);
// Fallback: escape special characters manually (loses formatting)
return escapeMarkdownV2(markdown);
}
}

View File

@@ -347,14 +347,24 @@ export class TelegramAdapter implements ChannelAdapter {
async sendMessage(msg: OutboundMessage): Promise<{ messageId: string }> {
const { markdownToTelegramV2 } = await import('./telegram-format.js');
// Convert markdown to Telegram MarkdownV2 format
// Try MarkdownV2 first
try {
const formatted = await markdownToTelegramV2(msg.text);
const result = await this.bot.api.sendMessage(msg.chatId, formatted, {
parse_mode: 'MarkdownV2',
reply_to_message_id: msg.replyToMessageId ? Number(msg.replyToMessageId) : undefined,
});
return { messageId: String(result.message_id) };
} catch (e) {
// If MarkdownV2 fails, send raw text with notice
console.warn('[Telegram] MarkdownV2 send failed, falling back to raw text:', e);
const errorMsg = e instanceof Error ? e.message : String(e);
const fallbackText = `${msg.text}\n\n(Telegram formatting failed: ${errorMsg.slice(0, 50)}. Report: github.com/letta-ai/lettabot/issues)`;
const result = await this.bot.api.sendMessage(msg.chatId, fallbackText, {
reply_to_message_id: msg.replyToMessageId ? Number(msg.replyToMessageId) : undefined,
});
return { messageId: String(result.message_id) };
}
}
async sendFile(file: OutboundFile): Promise<{ messageId: string }> {

View File

@@ -549,24 +549,25 @@ export class LettaBot {
// Only show "no response" if we never sent anything
if (!sentAnyMessage) {
if (!receivedAnyData) {
// Stream timed out with NO data at all - likely stuck approval or connection issue
console.error('[Bot] Stream received NO DATA - possible stuck tool approval');
// Stream timed out with NO data at all - likely stuck state
console.error('[Bot] Stream received NO DATA - possible stuck state');
console.error('[Bot] Agent:', this.store.agentId);
console.error('[Bot] Conversation:', this.store.conversationId);
console.error('[Bot] This can happen when a previous session disconnected mid-tool-approval');
console.error('[Bot] Recovery will be attempted automatically on the next message.');
await adapter.sendMessage({
chatId: msg.chatId,
text: '(Session interrupted. Please try your message again - recovery in progress.)',
text: '(Session interrupted. Try: lettabot reset-conversation)',
threadId: msg.threadId
});
} else {
console.warn('[Bot] Stream received data but no assistant message');
console.warn('[Bot] Message types received:', msgTypeCounts);
console.warn('[Bot] This may indicate: ADE session conflict, agent processing, or internal error');
// Give user informative message - avoid suggesting reset
console.warn('[Bot] Agent:', this.store.agentId);
console.warn('[Bot] Conversation:', this.store.conversationId);
const convIdShort = this.store.conversationId?.slice(0, 8) || 'none';
await adapter.sendMessage({
chatId: msg.chatId,
text: '(Agent is processing but returned no response. Please try again.)',
text: `(No response. Conversation: ${convIdShort}... Try: lettabot reset-conversation)`,
threadId: msg.threadId
});
}