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:
1735
package-lock.json
generated
1735
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -56,7 +56,7 @@
|
|||||||
"open": "^11.0.0",
|
"open": "^11.0.0",
|
||||||
"openai": "^6.17.0",
|
"openai": "^6.17.0",
|
||||||
"qrcode-terminal": "^0.12.0",
|
"qrcode-terminal": "^0.12.0",
|
||||||
"telegram-markdown-v2": "^0.0.4",
|
"telegramify-markdown": "^1.0.0",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"update-notifier": "^7.3.1",
|
"update-notifier": "^7.3.1",
|
||||||
|
|||||||
43
src/channels/telegram-format.test.ts
Normal file
43
src/channels/telegram-format.test.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,13 +11,13 @@
|
|||||||
*/
|
*/
|
||||||
export async function markdownToTelegramV2(markdown: string): Promise<string> {
|
export async function markdownToTelegramV2(markdown: string): Promise<string> {
|
||||||
try {
|
try {
|
||||||
// Dynamic import to avoid ESM/CommonJS compatibility issues
|
// Dynamic import to handle ESM module
|
||||||
const { convert } = await import('telegram-markdown-v2');
|
const telegramifyMarkdown = (await import('telegramify-markdown')).default;
|
||||||
// Use 'keep' strategy to preserve blockquotes (>) and other elements
|
// Use 'keep' strategy to preserve blockquotes (>) and other unsupported elements
|
||||||
return convert(markdown, 'keep');
|
return telegramifyMarkdown(markdown, 'keep');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[Telegram] Markdown conversion failed, using fallback:', e);
|
console.error('[Telegram] Markdown conversion failed, using escape fallback:', e);
|
||||||
// Fallback: escape special characters manually
|
// Fallback: escape special characters manually (loses formatting)
|
||||||
return escapeMarkdownV2(markdown);
|
return escapeMarkdownV2(markdown);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -347,14 +347,24 @@ export class TelegramAdapter implements ChannelAdapter {
|
|||||||
async sendMessage(msg: OutboundMessage): Promise<{ messageId: string }> {
|
async sendMessage(msg: OutboundMessage): Promise<{ messageId: string }> {
|
||||||
const { markdownToTelegramV2 } = await import('./telegram-format.js');
|
const { markdownToTelegramV2 } = await import('./telegram-format.js');
|
||||||
|
|
||||||
// Convert markdown to Telegram MarkdownV2 format
|
// Try MarkdownV2 first
|
||||||
const formatted = await markdownToTelegramV2(msg.text);
|
try {
|
||||||
|
const formatted = await markdownToTelegramV2(msg.text);
|
||||||
const result = await this.bot.api.sendMessage(msg.chatId, formatted, {
|
const result = await this.bot.api.sendMessage(msg.chatId, formatted, {
|
||||||
parse_mode: 'MarkdownV2',
|
parse_mode: 'MarkdownV2',
|
||||||
reply_to_message_id: msg.replyToMessageId ? Number(msg.replyToMessageId) : undefined,
|
reply_to_message_id: msg.replyToMessageId ? Number(msg.replyToMessageId) : undefined,
|
||||||
});
|
});
|
||||||
return { messageId: String(result.message_id) };
|
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 }> {
|
async sendFile(file: OutboundFile): Promise<{ messageId: string }> {
|
||||||
|
|||||||
@@ -549,24 +549,25 @@ export class LettaBot {
|
|||||||
// Only show "no response" if we never sent anything
|
// Only show "no response" if we never sent anything
|
||||||
if (!sentAnyMessage) {
|
if (!sentAnyMessage) {
|
||||||
if (!receivedAnyData) {
|
if (!receivedAnyData) {
|
||||||
// Stream timed out with NO data at all - likely stuck approval or connection issue
|
// Stream timed out with NO data at all - likely stuck state
|
||||||
console.error('[Bot] Stream received NO DATA - possible stuck tool approval');
|
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] Conversation:', this.store.conversationId);
|
||||||
console.error('[Bot] This can happen when a previous session disconnected mid-tool-approval');
|
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({
|
await adapter.sendMessage({
|
||||||
chatId: msg.chatId,
|
chatId: msg.chatId,
|
||||||
text: '(Session interrupted. Please try your message again - recovery in progress.)',
|
text: '(Session interrupted. Try: lettabot reset-conversation)',
|
||||||
threadId: msg.threadId
|
threadId: msg.threadId
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.warn('[Bot] Stream received data but no assistant message');
|
console.warn('[Bot] Stream received data but no assistant message');
|
||||||
console.warn('[Bot] Message types received:', msgTypeCounts);
|
console.warn('[Bot] Message types received:', msgTypeCounts);
|
||||||
console.warn('[Bot] This may indicate: ADE session conflict, agent processing, or internal error');
|
console.warn('[Bot] Agent:', this.store.agentId);
|
||||||
// Give user informative message - avoid suggesting reset
|
console.warn('[Bot] Conversation:', this.store.conversationId);
|
||||||
|
const convIdShort = this.store.conversationId?.slice(0, 8) || 'none';
|
||||||
await adapter.sendMessage({
|
await adapter.sendMessage({
|
||||||
chatId: msg.chatId,
|
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
|
threadId: msg.threadId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user