Skip to content

Telegram channel blocks startup: bot.launch() never resolves #14

@Arthur-GH

Description

@Arthur-GH

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

  1. Configure channels.yaml with only Telegram enabled
  2. Start Phantom
  3. Observe logs: "Telegram channel registered" appears, but "Bot connected via long polling" and "{name} is ready" never appear
  4. curl /health shows "telegram": false
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions