@@ -84,52 +84,87 @@ async def _start_agent(config_store: ConfigStore):
8484 )
8585 agent .voice = voice
8686
87- # -- Telegram --
88- async def _start_tg (tg : TelegramChannel , name : str ) -> None :
89- agent .channels [name ] = tg
90- log .info ("Starting Telegram bot (%s)…" , name )
91- await tg .app .initialize ()
92- await tg .app .start ()
93- if tg .app .updater is not None :
94- await tg .app .updater .start_polling ()
95-
96- tg_global = config .channels .telegram
97- if tg_global .enabled and tg_global .bot_token :
98- await _start_tg (TelegramChannel (tg_global , agent , voice = voice ), "telegram" )
99-
100- # -- Per-persona Telegram bots (#29): each persona with its own token is its
101- # own contact. Channel name "telegram:<persona>" silos history and resolves
102- # straight to that persona. ACL inherits the global allowlist when unset.
103- for persona in await agent .personae .list_personae ():
104- token = (persona .bot_token or "" ).strip ()
105- if not token or token == tg_global .bot_token :
106- continue # no token, or shares the default bot's token — skip
107- pconf = TelegramConfig (
108- enabled = True ,
109- bot_token = token ,
110- allowed_user_ids = persona .allowed_user_ids or tg_global .allowed_user_ids ,
111- topics_enabled = tg_global .topics_enabled ,
112- )
113- await _start_tg (
114- TelegramChannel (pconf , agent , voice = voice , channel_name = f"telegram:{ persona .name } " ),
115- f"telegram:{ persona .name } " ,
116- )
87+ # -- Telegram: the default bot plus one bot per persona that carries a token (#29).
88+ # A single bad/revoked token must never abort the others, WhatsApp, or the
89+ # scheduler — each bot is brought up independently and failures are isolated.
90+ async def _start_tg (conf , name : str , channel_name : str = "telegram" ) -> None :
91+ try :
92+ tg = TelegramChannel (conf , agent , voice = voice , channel_name = channel_name )
93+ await tg .app .initialize ()
94+ await tg .app .start ()
95+ if tg .app .updater is not None :
96+ await tg .app .updater .start_polling ()
97+ agent .channels [name ] = tg # registered only once it is actually polling
98+ log .info ("Telegram bot started (%s)" , name )
99+ except Exception :
100+ log .exception ("Failed to start Telegram bot %s — skipping" , name )
101+
102+ try :
103+ tg_global = config .channels .telegram
104+ seen_tokens : set [str ] = set ()
105+ if tg_global .enabled and tg_global .bot_token :
106+ seen_tokens .add (tg_global .bot_token )
107+ await _start_tg (tg_global , "telegram" )
108+
109+ for persona in await agent .personae .list_personae ():
110+ token = (persona .bot_token or "" ).strip ()
111+ if not token :
112+ continue # no own bot — reachable only via the default bot
113+ if token in seen_tokens :
114+ log .warning (
115+ "Persona %s shares a bot token with another bot — skipping its bot "
116+ "(one token can only be polled once)" ,
117+ persona .name ,
118+ )
119+ continue
120+ seen_tokens .add (token )
121+ pconf = TelegramConfig (
122+ enabled = True ,
123+ bot_token = token ,
124+ allowed_user_ids = persona .allowed_user_ids or tg_global .allowed_user_ids ,
125+ topics_enabled = tg_global .topics_enabled ,
126+ )
127+ await _start_tg (pconf , f"telegram:{ persona .name } " , f"telegram:{ persona .name } " )
128+
129+ # -- WhatsApp --
130+ if config .channels .whatsapp .enabled :
131+ from core .wacli import WacliManager
132+
133+ wacli = WacliManager ()
134+ wa = WhatsAppChannel (config .channels .whatsapp , agent , wacli = wacli )
135+ agent .channels ["whatsapp" ] = wa
136+ log .info ("WhatsApp channel enabled (wacli)" )
137+
138+ # -- Scheduler --
139+ await agent .scheduler .load_jobs ()
140+ agent .scheduler .start ()
141+ log .info ("Scheduler started with %d jobs" , len (agent .scheduler .scheduler .get_jobs ()))
142+ except Exception :
143+ # Bring-up failed after some bots were already polling — stop them so we
144+ # don't leak orphaned pollers (which would 409 on the next start).
145+ await _stop_telegram_bots (agent )
146+ raise
117147
118- # -- WhatsApp --
119- if config .channels .whatsapp .enabled :
120- from core .wacli import WacliManager
148+ return agent
121149
122- wacli = WacliManager ()
123- wa = WhatsAppChannel (config .channels .whatsapp , agent , wacli = wacli )
124- agent .channels ["whatsapp" ] = wa
125- log .info ("WhatsApp channel enabled (wacli)" )
126150
127- # -- Scheduler --
128- await agent .scheduler .load_jobs ()
129- agent .scheduler .start ()
130- log .info ("Scheduler started with %d jobs" , len (agent .scheduler .scheduler .get_jobs ()))
151+ async def _stop_telegram_bots (agent ) -> None :
152+ """Stop and deregister the default bot and every per-persona bot (#29).
131153
132- return agent
154+ Each bot is torn down independently: one that fails to stop must not strand
155+ the rest still polling (which would 409 on the next start).
156+ """
157+ for name , ch in list (agent .channels .items ()):
158+ if name != "telegram" and not name .startswith ("telegram:" ):
159+ continue
160+ try :
161+ if ch .app .updater is not None :
162+ await ch .app .updater .stop ()
163+ await ch .app .stop ()
164+ await ch .app .shutdown ()
165+ except Exception :
166+ log .exception ("Error stopping Telegram bot %s" , name )
167+ agent .channels .pop (name , None )
133168
134169
135170async def _stop_agent (agent ) -> None :
@@ -140,12 +175,7 @@ async def _stop_agent(agent) -> None:
140175
141176 set_agent_context (None )
142177
143- # Stop the default bot and every per-persona bot ("telegram:<persona>", #29).
144- for name , ch in list (agent .channels .items ()):
145- if name == "telegram" or name .startswith ("telegram:" ):
146- await ch .app .updater .stop ()
147- await ch .app .stop ()
148- await ch .app .shutdown ()
178+ await _stop_telegram_bots (agent )
149179
150180
151181# ---------------------------------------------------------------------------
0 commit comments