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 run
setTriggerDeps() — /trigger endpoint never wired
setSecretSavedCallback() — secret save notifications never wired
console.log("is ready") — never printed
connectionState stays "connecting" → health endpoint shows telegram: 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.yaml with only Telegram enabled
- Start Phantom
- Observe logs: "Telegram channel registered" appears, but "Bot connected via long polling" and "{name} is ready" never appear
curl /health shows "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.
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, theconnect()method awaitsbot.launch():Telegraf's
launch()callsstartPolling()→polling.loop(), which is an infinite async iterator (do { getUpdates } while (!aborted)) that only resolves whenbot.stop()is called. Soconnect()never returns.Since
router.connectAll()usesPromise.allSettled(), it waits for all channel promises — meaning everything afterconnectAll()inindex.tsis 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: falseMessages still work because Telegraf yields control during
await callApi('getUpdates'), so registered handlers fire for incoming updates even though the promise never resolves.Environment
ghostwright/phantom:latest)Reproduction
channels.yamlwith only Telegram enabledcurl /healthshows"telegram": falseSuggested Fix
Don't await
bot.launch()— fire and forget with error handling: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'slaunch()resolves only when polling stops.