-
Notifications
You must be signed in to change notification settings - Fork 106
Telegram channel blocks startup: bot.launch() never resolves #14
Description
Bug
When Telegram is the only channel configured, router.connectAll() hangs forever, blocking the scheduler, trigger endpoint, and the "is ready" log.
Root Cause
In src/channels/telegram.ts, the connect() method awaits bot.launch():
async connect(): Promise<void> {
// ...
await this.bot.launch(); // ← hangs forever
this.connectionState = "connected"; // ← never reached
}Telegraf's launch() calls startPolling() → polling.loop(), which is an infinite async iterator (do { getUpdates } while (!aborted)) that only resolves when bot.stop() is called. So connect() never returns.
Since router.connectAll() uses Promise.allSettled(), it waits for all channel promises — meaning everything after connectAll() in index.ts is never executed:
scheduler.start()(line ~600) — scheduled tasks never runsetTriggerDeps()—/triggerendpoint never wiredsetSecretSavedCallback()— secret save notifications never wiredconsole.log("is ready")— never printedconnectionStatestays"connecting"→ health endpoint showstelegram: false
Messages still work because Telegraf yields control during await callApi('getUpdates'), so registered handlers fire for incoming updates even though the promise never resolves.
Environment
- Phantom v0.18.1 (Docker,
ghostwright/phantom:latest) - Telegraf 4.16.3
- Telegram-only config (no Slack)
- Hetzner CX33, Bun runtime
Reproduction
- Configure
channels.yamlwith only Telegram enabled - Start Phantom
- Observe logs: "Telegram channel registered" appears, but "Bot connected via long polling" and "{name} is ready" never appear
curl /healthshows"telegram": false- Messages work fine despite the above
Suggested Fix
Don't await bot.launch() — fire and forget with error handling:
async connect(): Promise<void> {
if (this.connectionState === "connected") return;
this.connectionState = "connecting";
try {
const { Telegraf } = await import("telegraf");
this.bot = new Telegraf(this.config.botToken) as unknown as TelegrafBot;
this.registerHandlers();
// Don't await — launch() runs the polling loop forever
this.bot.launch().catch((err: unknown) => {
this.connectionState = "error";
const msg = err instanceof Error ? err.message : String(err);
console.error(`[telegram] Polling error: ${msg}`);
});
this.connectionState = "connected";
console.log("[telegram] Bot connected via long polling");
} catch (err: unknown) {
this.connectionState = "error";
const msg = err instanceof Error ? err.message : String(err);
console.error(`[telegram] Failed to connect: ${msg}`);
throw err;
}
}This matches how Telegraf is typically used — launch() is documented as a long-running call that starts the bot, not something you await to completion.
Note: The Slack channel likely doesn't have this issue because Socket Mode's app.start() resolves after the WebSocket handshake, whereas Telegraf's launch() resolves only when polling stops.