[BUG] Telegram plugin auto-loads in all Claude Code sessions, not just --channels sessions

Open 💬 23 comments Opened Mar 24, 2026 by neuralneeraj

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

When the Telegram plugin is installed, every claude -c session (without --channels flag) automatically spawns a Telegram bun process that polls for messages. This causes multiple consumers competing for the same Telegram bot token, resulting in dropped/lost messages.

Steps to reproduce:

  1. Install Telegram plugin: claude plugin add telegram@claude-plugins-official
  2. Start session A: claude --channels plugin:telegram@claude-plugins-official
  3. Start session B: claude -c (no --channels flag)
  4. Both sessions spawn separate Telegram bun processes polling the same bot token
  5. Messages are randomly delivered to either session — ~50% message loss on the intended session

What Should Happen?

Expected behavior:
Only sessions explicitly started with --channels plugin:telegram@claude-plugins-official should spawn the Telegram plugin process. Plain claude -c sessions should not load channel plugins.

Actual behavior:
All sessions load installed plugins regardless of --channels flag. Each spawns its own bun server.ts process for the Telegram plugin, creating competing consumers on the same bot token.

Workaround:
Manually kill the extra bun/server.ts processes spawned by non-channel sessions.

Impact: Critical for Telegram users — messages are silently lost with no error indication.

Error Messages/Logs

Steps to Reproduce

  1. Install Telegram plugin: /install telegram@claude-plugins-official
  2. Start terminal A: claude --channels plugin:telegram@claude-plugins-official
  3. Start terminal B: claude -c (no --channels flag)
  4. Run: ps aux | grep "server.ts" — shows TWO telegram bun processes
  5. Send a Telegram message — it randomly goes to terminal A or B
  6. ~50% of messages silently lost on the intended session

Claude Model

Opus

Is this a regression?

No, this never worked

Last Working Version

_No response_

Claude Code Version

2.1.81 (Claude Code)

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Terminal.app (macOS)

Additional Information

_No response_

View original on GitHub ↗

23 Comments

bujosa · 3 months ago

same happen with me

liysx · 3 months ago

I'm also experiencing this issue. After exiting Claude Code, the Telegram plugin's bun process is not terminated and keeps running until CPU hits 100%.

Environment:

  • Claude Code version: 2.1.81
  • Telegram plugin version: 0.0.4
  • Terminal: Ghostty
  • Hardware: Mac mini M4 Pro
  • OS: macOS

The orphaned bun server.ts process from the Telegram channel plugin persists after session exit and gradually consumes all available CPU resources.

SG5 · 3 months ago
running until CPU hits 100%.

@liysx try to update to 2.1.83

neuralneeraj · 3 months ago

@SG5 Still the same issue

liysx · 3 months ago

The core issue persists on v2.1.83 — every new Claude Code session still spawns a Telegram plugin process regardless of whether --channels was passed.
Each bun process now terminates properly when its parent session exits. The number of running Telegram plugin processes matches the number of active sessions exactly.
The practical impact remains: multiple competing consumers on the same bot token cause messages to be randomly routed to unintended sessions, resulting in probabilistic message loss.

rzblues · 3 months ago

Confirmed + Root Cause + Reference Implementation

Reproducing this on macOS 15 (Darwin 25.3.0 arm64) with Claude Code v2.1.85 + VSCode extension. The VSCode sidebar panel also loads the Telegram plugin even without --channels, causing the same competing-consumers problem.

Root cause (from source inspection)

From cli.js, the plugin loading path (Ol_()) reads enabledPlugins from ~/.claude/settings.json at startup and unconditionally starts the MCP server for every matching plugin — regardless of whether --channels was passed. The --channels flag is a separate subsystem: it only registers an already-running MCP server for inbound channel notifications. So:

  • enabledPlugins: true → every CC instance starts the bot (polling begins)
  • --channels → marks that instance as the one that should handle inbound pushes

Both are required for the channel to work, but the decoupling means every instance starts polling, even though only one should.

Workaround (better than killing processes)

Add "telegram@claude-plugins-official": false to a project-level .claude/settings.local.json in the working directory used by your non-channel instances (e.g. your VSCode project root). This overrides the global enabledPlugins for that instance only.

Reference implementation: OpenClaw

OpenClaw (github.com/openclaw/openclaw) — an open-source AI messaging gateway that also runs Telegram bots — handles this exact problem cleanly. Their approach (extensions/telegram/src/):

  1. Update offset persistence (update-offset-store.ts): stores { lastUpdateId, botId } to disk, validates botId on load to reject stale offsets from a different token
  2. In-flight deduplication (bot.ts): tracks which update IDs are currently being processed; only advances the "safe watermark" below the smallest in-flight ID — prevents skipping updates under concurrent processing
  3. In-memory dedup (createTelegramUpdateDedupe()): secondary guard within a session

Result: multiple instances can safely share the same bot token. Each receives every update, but deduplication ensures each is processed exactly once — no message loss.

This is the pattern I'd suggest as a reference for fixing the plugin-level conflict here.

bujosa · 3 months ago

I will update to the new version and trial again

nickcarmonadigital · 3 months ago

Simpler Global Workaround + Architectural Proposal

So I am running a Telegram bot via --channels in a dedicated tmux session. Meanwhile, Claude Desktop's Code tab had a week-old session that was never configured with --channels. That session started receiving and responding to my Telegram messages using its own unrelated context. Messages were routed to the wrong session with zero indication.

Evidence:

  • 12 Claude Desktop Code tab processes, all with --plugin-dir .../telegram/0.0.4 in their args
  • 0 of them had --channels
  • All 12 were competing with my tmux session for Telegram polling
  • enabledPlugins: {} (empty) did NOT prevent Desktop from loading the plugin
  • Desktop reads installed_plugins.json and passes --plugin-dir for every installed plugin regardless

of enabledPlugins

Current workaround I found with claude:

The existing workaround from @rzblues (per-project settings.local.json) requires adding an override to every project directory. Misses any new project. Which I will add, @rzblues suggestion did help me a ton with figuring this out as well.

Our approach — one global setting:

// ~/.claude/settings.json
{
"enabledPlugins": {
"telegram@claude-plugins-official": false
}
}

Then launch the dedicated channel session with:

claude --channels plugin:telegram@claude-plugins-official

Why this works: --channels loads and activates the plugin independently of enabledPlugins. Setting
enabledPlugins: false globally prevents every other session (including Claude Desktop Code tab,
VSCode sidebar, subagents, scheduled tasks) from loading the plugin. Only the session with
--channels polls Telegram.

Verified:

  • ps aux | grep telegram returns 0 for all Desktop sessions
  • Telegram bot responds only through the --channels session
  • New Desktop Code tab sessions don't load the plugin
  • Survives Claude Desktop restart
  • Survives tmux session restart via watchdog

Important: enabledPlugins: {} (empty object) is NOT the same as enabledPlugins: { "telegram@...":
false }. Empty means "use defaults" — Desktop still loads installed plugins. Explicit false is
required.

Also important: claude plugin install resets enabledPlugins back to true. After any plugin
reinstall/update, you must manually set it back to false.

Additional Issues We Hit

  1. CLAUBBIT=1 env var — needed to skip the workspace trust dialog in headless/tmux sessions. Without

it, the trust prompt blocks headless restarts and the watchdog crash-loops.

  1. macOS TCC prompt — tmux launched by LaunchAgent triggers "tmux would like to access data from

other apps" on every restart. Fix: add tmux + terminal app to System Settings → Privacy & Security →
Full Disk Access.

  1. claude setup-token OAuth changes billing mode — the token from setup-token authenticates as

"Claude API" (Sonnet 4.6) instead of "Claude Max" (Opus 4.6). Removing the CLAUDE_CODE_OAUTH_TOKEN
env var and using normal login auth restores Max plan access.

### _Architectural Proposal: Named Session Channel Routing_

The root issue is that channel plugins have no concept of "which session should I serve." The
current architecture:

enabledPlugins: true → every session starts polling
--channels → marks one session for notifications (but polling already started
everywhere)

Proposed: session-scoped channel assignment.

// ~/.claude/settings.json
{
"channelSessions": {
"telegram-bot": {
"channel": "plugin:telegram@claude-plugins-official",
"autostart": true
},
"discord-bot": {
"channel": "plugin:discord@claude-plugins-official",
"autostart": true
},
"imessage-bot": {
"channel": "plugin:imessage@claude-plugins-official",
"autostart": false
}
}
}

Or at minimum, --channels should be the ONLY mechanism that starts the plugin's MCP server polling.
If a session doesn't have --channels, the plugin should not start — even if it's in enabledPlugins
or installed_plugins.json. The enabledPlugins flag should control tool availability only, not
polling process startup.

This would also enable multi-channel architectures where one machine runs multiple named sessions —
one for Telegram, one for Discord, one for iMessage — each isolated, each with its own context.

Environment

  • Claude Code v2.1.86
  • macOS 15.3 (Apple Silicon)
  • Claude Max plan (individual)
  • Claude Desktop v1.1.9310
  • Telegram plugin v0.0.4
  • Always-on via tmux + LaunchAgent watchdog
neuralneeraj · 3 months ago

Workaround seems to be working fine for the last 2 days.

  1. Removed telegram@claude-plugins-official from global ~/.claude/settings.jsonenabledPlugins
  2. Created a project-scoped .claude/settings.json in the specific workspace where I want Telegram, with {"enabledPlugins": {"telegram@claude-plugins-official": true}}.

This way, only sessions running in that project directory start the Telegram MCP server. Other Claude sessions in different projects don't spawn zombie bun processes.

Will close this if stable after 7 days.

rzblues · 3 months ago
Workaround seems to be working fine for the last 2 days. 1. Removed telegram@claude-plugins-official from global ~/.claude/settings.jsonenabledPlugins 2. Created a project-scoped .claude/settings.json in the specific workspace where I want Telegram, with {"enabledPlugins": {"telegram@claude-plugins-official": true}}. This way, only sessions running in that project directory start the Telegram MCP server. Other Claude sessions in different projects don't spawn zombie bun processes. Will close this if stable after 7 days.

Please don't close this as this is just a workaround. I would like the Claude code telegram to run on my root folder

neuralneeraj · 3 months ago

@rzblues okay, will keep it open

patrick-yip · 3 months ago

Since I run multiple sessions in the same project, the enabledPlugins approach doesn't work for me — all sessions inherit the same setting. Took a different approach with a SessionStart hook that cleans up at the process level instead: https://github.com/patrick-yip/claude-telegram-guard

On every session start, it finds all bun processes running the Telegram plugin, kills any whose parent Claude process doesn't have --channels, and keeps only the newest if multiple legit sessions exist. Tested on macOS.

Suggest keeping this open until Anthropic fixes plugin loading in non-channel sessions at the source.

maciek-hyperdev · 3 months ago

Workaround: use --dangerously-load-development-channels server:<name> instead of --channels plugin:<name>@<marketplace>

We've confirmed that the --channels plugin:telegram@claude-plugins-official path does not deliver inbound notifications/claude/channel to the session, while a bare .mcp.json server entry with --dangerously-load-development-channels works end-to-end on the same machine, same token, same Claude Code version (2.1.92).

Steps

  1. Add the plugin's MCP server directly to your project's .mcp.json (or ~/.claude.json for global):
{
  "mcpServers": {
    "telegram": {
      "command": "bun",
      "args": [
        "run", "--cwd",
        "/Users/<you>/.claude/plugins/cache/claude-plugins-official/telegram/0.0.4",
        "--shell=bun", "--silent", "start"
      ]
    }
  }
}

(Adjust the version path to match your cached plugin version.)

  1. Launch with the development flag instead of --channels:
claude --dangerously-load-development-channels server:telegram
  1. Send a message from Telegram — it arrives as a <channel source="telegram" ...> tag and Claude responds via the reply tool.

Why this works

The --dangerously-load-development-channels server:<name> flag activates channel routing for a bare MCP server entry (keyed by the name in .mcp.json). It bypasses the plugin resolution + allowlist path that --channels plugin:<name>@<marketplace> uses. The MCP server itself is identical — same binary, same capability negotiation — but the CLI-side activation path is different and apparently not affected by the bug.

Environment

  • Claude Code 2.1.92
  • macOS 15.4 (Darwin 25.3.0, arm64)
  • Telegram plugin 0.0.4
  • Auth: claude.ai OAuth (not API key)

This also works for Discord and custom channel servers — any plugin that advertises experimental.claude/channel can be registered as a bare .mcp.json entry and activated with --dangerously-load-development-channels server:<name>.

maciek-hyperdev · 3 months ago

Update: We've published a full Rust drop-in replacement for the official Bun-based Telegram plugin: hdcd-telegram (v0.1.0).

  • 3.5 MB static binary vs ~100 MB Bun runtime
  • ~5 MB RAM per instance vs ~100 MB
  • <50 ms startup vs 2-3 seconds
  • Immediate shutdown on stdin EOF (no zombie 409 Conflict)
  • Full feature parity: all 8 message types, 4 tools, access control, permission relay, voice transcription via whisper
  • Compatible with existing access.json and .env — zero migration

Works with the --dangerously-load-development-channels server:telegram workaround described above. Pre-built binaries for Linux, macOS (Intel + Apple Silicon), and Windows available on the releases page.

noahzweben · 3 months ago

Hi - we've put in some fixes to channels that address zombie bun server.ts pollers / 409 Conflict. Please update your channel version for telegram to 0.0.6.

---
_Generated by Claude Code_

liysx · 3 months ago

Seeing the same class of issue on a newer Claude Code build. Reporting my environment in case it helps narrow things down:

  • Claude Code: 2.1.108 (Claude Code)
  • Telegram plugin: telegram@claude-plugins-official v0.0.6
  • Platform: macOS (Darwin 25.4.0)
  • Plugin scope: enabled at project level via .claude/settings.json:

``json
{
"enabledPlugins": {
"telegram@claude-plugins-official": true
}
}
``

When launching with the documented flag:

claude --channels plugin:telegram@claude-plugins-official

Claude Code prints:

--channels ignored (plugin:telegram@claude-plugins-official)
Channels are not currently available

So on 2.1.108 the --channels flag appears to be rejected outright (Channels are not currently available), which makes the original workaround in this thread ("only start the telegram consumer when --channels is passed") impossible to apply — any session with the plugin enabled at user/project level still auto-spawns the bun server.ts consumer, and there's no supported way to opt in per session.

Happy to provide additional logs / ps output if useful.

mahi97 · 3 months ago

Specific sub-case: Agent Team (TeamCreate) spawns competing Telegram instances

We're hitting this with the Agent Team feature. The reproduction path:

  1. Use TeamCreate to create a team, then spawn teammates via Agent with team_name parameter
  2. Each teammate is launched in its own tmux pane
  3. Each teammate inherits the parent's --channels plugin:telegram@claude-plugins-official flag
  4. Each teammate's process spawns its own bun server.ts for the Telegram plugin
  5. Multiple bun processes now compete for getUpdates on the same bot token → 409 Conflict loops, missed messages, duplicated deliveries

This is a specific and reliable reproduction case that may not be obvious from the general "multiple instances" framing — the user only starts one Claude Code session, but the team infrastructure multiplies it.

Current workaround: A SubagentStart hook that detects and kills competing bun server.ts instances before a new teammate starts polling. This is fragile — it races with the teammate's own startup, and killing a process that's mid-reply can drop outgoing messages.

Better fix: The PID lock approach from #39876 would solve this cleanly. Only the first teammate's Telegram process polls; all others serve tools only (outgoing replies via bot.api.sendMessage still work without polling). We've applied this locally and it works well with 3–5 teammates sharing a single bot token.

CalebC83 · 3 months ago

Confirmed reproduction on Windows 10 (Claude Code 2.1.101, VS Code extension + CLI).

Mechanism, pinned to specific PIDs from a single run:

  1. 10:50:46 CLI claude --channels plugin:telegram@... spawns bun server.ts PID 132572. Writes its PID to bot.pid. Polls getUpdates successfully, Telegram bridge works.
  2. 11:04:29 Open a Claude Code session in VS Code (no --channels flag). The extension loads the Telegram plugin anyway. Its bun server.ts (PID 4672) starts, reads bot.pid, sees the alive incumbent, and issues SIGTERM against it (server.ts lines 59-69 in v0.0.6). PID 132572 dies.
  3. The CLI session's MCP stdio pipe closes when its bun exits. No reconnect logic fires. The CLI session shows Telegram as disconnected and messages stop being delivered.
  4. Eventually the VS Code session closes, PID 4672's orphan watchdog fires, it shuts down and deletes bot.pid. Now nothing is running, and the original --channels session still has no Telegram bridge.

So the user-facing symptom ("Telegram disconnects randomly after a couple of hours") is actually: any other Claude Code session loading the plugin kills the legitimate poller.

Local mitigation (v0.0.6): inserted a startup guard in server.ts before the PID-file logic that walks the process ancestry (WMI on Windows, /proc on Linux, ps on macOS), finds the nearest claude/claude.exe ancestor, and checks its command line for --channels. If absent, respond to MCP initialize with empty capabilities and exit(0), never touching bot.pid. --channels sessions run the existing code path. Fails open on ancestry-walk errors.

Result in a controlled reproduction: opening a VS Code session spawned 4 launcher + 4 server.ts buns (multiple VS Code sessions were open). All 4 server.ts processes yielded and exited within ~10 seconds. The --channels session's bun survived untouched and continued receiving Telegram messages. End-to-end tested.

Happy to share the diff if useful. The cleaner upstream fix discussed in #36960 (plugin singleton mode) would obsolete this.

bryanhong · 2 months ago

+1, confirming on Claude Code 2.1.117 (macOS 26.3.1).

Symptom we were chasing: in session A (started with --channels plugin:telegram), the telegram MCP shows "disconnected" in a <system-reminder> and /mcp reports the server as gone. Every time we reconnect, it stays up for a while then drops again.

Trigger: starting or reconnecting in session B. Session B was deliberately started without --channels, which (per the original assumption) should not load the plugin — but per this bug it does, silently. So session B auto-spawns its own bun server.ts, calls getUpdates on the same bot token, and Telegram's 409-Conflict behavior kicks whichever long-poller was holding the lock. Last session to start (or to run /mcp) wins, the other's plugin exits.

That explains the ghost symptoms we saw:

  • ps aux | grep server.ts often shows only one telegram process — the loser already exited by the time you check.
  • On /mcp reconnect in session A, stderr logs telegram channel: replacing stale poller pid=<old> — confirming the previous long-poller is being displaced.
  • No error is logged on the losing side; the MCP log file just stops.

Only workaround needed: if session A is the one you want reachable via Telegram, just /mcp in session A after session B has started — last-writer-wins means A takes the lock back and stays up until session B does the same. Fragile but self-healing without any pkill.

Real fix is what the OP suggests — plugins declared under --channels should only load in sessions started with --channels.

judewang · 2 months ago

I was troubled by this issue for a long time until I recently discovered that the problem wasn’t where I had thought it was.

By patching the server.ts file in the Telegram plugin, I have now completely resolved the issue. I can now use the latest version of the Telegram plugin without any disconnections from MCP.

Related issue: https://github.com/anthropics/claude-plugins-official/issues/1481
Closed PR: https://github.com/anthropics/claude-plugins-official/pull/1461

thevincentlan · 2 months ago

Adding a fresh data point on a newer build — bug persists in Claude Code 2.1.138 / Telegram plugin 0.0.6, and I hit a stronger failure mode than what's been reported above.

What I saw

Six bun server.ts processes from the telegram plugin had accumulated across sessions, each pegged at 100% CPU. Oldest was running since April 21 (~3 weeks of wall time, ~95 hours of CPU time). They survived parent-Claude-Code deaths and ignored SIGTERM — I had to SIGKILL all six.

So this is two failure modes layered on top of each other:

  1. The auto-load-everywhere problem already covered in the thread (every session spawns a bridge).
  2. Stuck-spinning zombies that ignore SIGTERM. Not just orphaned-but-idle — actively pegged at 100% CPU.

Code observations

The plugin's server.ts (v0.0.6) has three independent mechanisms intended to prevent exactly this — all three failed in my case:

  • PID-file guard at startup (lines 54-69) sends SIGTERM to any prior PID holder. In my case the prior holders ignored SIGTERM, so new sessions couldn't displace them. The guard never escalates to SIGKILL.
  • stdin EOF/close shutdown (lines 648-649): didn't fire — Claude Code's stdio pipes apparently stayed half-open when the parent died.
  • Orphan watchdog (lines 657-664) polls process.ppid !== bootPpid every 5s. Also didn't catch the reparent, for reasons I couldn't determine without more instrumentation.

The retry loop (lines 993-1032) has bounded backoff (max 15s), so the 100% CPU isn't from that loop spinning — my guess is grammy's getUpdates ended up in a state where its internal HTTP retry pattern was busy-looping (network hiccup + a specific error type?), and none of the three shutdown paths could fire to stop it.

Mitigation I'm running locally

For folks who genuinely need telegram in multiple concurrent sessions (so @nickcarmonadigital's enabledPlugins: false + --channels global workaround doesn't fit), a complementary defense is a SessionStart hook that kills any telegram bridge with sustained high CPU>50% AND age >5min. Healthy bridges idle near 0% CPU, so it spares concurrent sessions and only catches actual zombies. Happy to share the script if useful.

Suggestions for a real fix

  • Escalate SIGTERM → SIGKILL in the PID-file guard after a short timeout. Cheap and would have unblocked all six of mine on the next session start.
  • Add a self-health check that exits the process if CPU stays >50% sustained for, say, 30s (a healthy bridge is never CPU-bound).
  • Decouple loading from polling as @rzblues described — only the --channels session should call bot.start().
Sugumaran-Balasubramaniyan · 1 month ago

Filed #66309 independently: Diagnosed the root cause as the orphan watchdog checking process.ppid !== bootPpid, but missing the case where bun run (the direct parent) gets reparented to init when CC exits. server.ts's own ppid never changes, so the watchdog doesn't fire. Fix: read grandparent PID from /proc/<ppid>/status at startup and check for reparenting. Patch available in #66309.

davidjcode · 17 days ago

Another data point + a workaround not yet in this thread (token isolation)

Confirming the bug persists on Claude Code 2.1.195 / Telegram plugin 0.0.6 (macOS). On this build --channels plugin:telegram@claude-plugins-official does work for the dedicated session (contrary to the 2.1.108 "Channels are not currently available" report above), but the underlying problem is unchanged: every other claude process on the machine still auto-spawns its own bun server.ts poller.

Trigger worth adding to the list: a scheduled, always-on background agent on the same machine. I run a dedicated channel-host session (a supervisor that launches --channels) alongside a separate 24/7 agent that a launchd job relaunches every ~30–45 min. Each relaunch spawned a competing poller, and the plugin's stale-poller cleanup (server.ts reads bot.pid and SIGTERMs the incumbent) killed the host session's listener like clockwork — the competing spawn timestamps matched the listener deaths to the second. Same mechanism @CalebC83 and @bryanhong describe (replacing stale poller pid=), but driven by a scheduler rather than a human opening VS Code, so it recurs ~hourly, around the clock. A watchdog kept auto-restarting the host, which turned into ~20 "recovered" notifications overnight.

Workaround — token isolation (no per-project config, no plugin patching, no hooks):

server.ts reads the bot token and process.exit(1)s if it's missing — and that check runs before the PID-file stale-poller-kill. So if non-host sessions can't see the token, their pollers exit cleanly at startup and never touch bot.pid.

  1. Remove TELEGRAM_BOT_TOKEN from the shared ~/.claude/channels/telegram/.env.
  2. Provide it only to the channel-host session via its environment — e.g. the supervisor that launches the --channels session exports TELEGRAM_BOT_TOKEN (sourced from a private chmod-600 file) before launching claude. The plugin's "real env wins over .env" logic picks it up; confirmed in the spawned poller's environment via ps eww.

Result: the host session polls; every other session (VS Code, subagents, scheduled/background agents, plain claude) logs telegram channel: TELEGRAM_BOT_TOKEN required and exits immediately, leaving the host untouched.

Why I'd reach for this over the other workarounds in the thread:

  • No per-project .claude/settings.local.json to maintain across directories (and it sidesteps the settings.local.json merge quirk).
  • Survives claude plugin install/update, which resets enabledPlugins back to true.
  • Doesn't depend on --channels / enabledPlugins behavior at all, so it still holds on builds where those are rejected or unreliable.
  • No process-killing hook racing with a teammate/session mid-reply.

Verified end-to-end: the host survives a forced relaunch on the env-only token; a real competing claude -p in another directory logged TELEGRAM_BOT_TOKEN required and exited while the host's poller PID stayed put; inbound messages keep arriving.

Agreed with the thread that the real fix is decoupling load-from-poll — only sessions started with --channels should call bot.start().

---
_Drafted with Claude Code._