feat(gateway): multi-account Telegram — one gateway, N bots, isolated sessions (#8287) - #67455
feat(gateway): multi-account Telegram — one gateway, N bots, isolated sessions (#8287)#67455Hotragn wants to merge 8 commits into
Conversation
teknium1
left a comment
There was a problem hiding this comment.
Thanks for carrying forward the multi-account design from #10455. The current-main premise is real, but the inbound identity and lifecycle paths need correction before this can safely land.
Problems
- Normal Telegram events call
self.build_source()(plugins/platforms/telegram/adapter.py:8953), while the PR-head implementation returns aSessionSourcewithoutaccount(gateway/platforms/base.py:5746-5766). The two new Telegram stamps are only auth-helper sources (plugins/platforms/telegram/adapter.py:905,983), so ordinary named-bot traffic still gets the default session key and default egress adapter. - Named accounts start only after the default adapter connects (
gateway/run.py:7726-7742). A bad default token blocks otherwise healthy named bots. - Initial named-account connection failures are disconnected and dropped (
gateway/run.py:9985-9999), never entering the queue processed by the account reconnect watcher. hermes sendremains unable to targettelegram@support: it delegates throughtools/send_message_tool.py(hermes_cli/send_cmd.py:346-359), whose parser requires the platform segment itself to be aPlatform(tools/send_message_tool.py:362-406).
Suggested changes
- Stamp account identity in
BasePlatformAdapter.build_source()and add a real normal-event isolation/egress test. - Start and retry named adapters independently of default-adapter success.
- Add account-aware
hermes sendrouting or narrow the documented target surface.
Automated hermes-sweeper review.
| @@ -979,6 +980,7 @@ def _source_from_message_for_auth(self, message: Message): | |||
| user_id=user_id, | |||
| user_name=user_name, | |||
| thread_id=thread_id, | |||
| account=getattr(self, "account_name", None), | |||
There was a problem hiding this comment.
This stamps only _source_from_message_for_auth. Normal Telegram traffic is built by _build_message_event() through inherited BasePlatformAdapter.build_source(), whose returned SessionSource still has no account; named-account messages therefore retain account=None, share the default session key, and route replies through the default adapter. Propagate self.account_name in build_source() and cover the normal event path.
| # Named bot accounts on this platform (#8287) start after | ||
| # the default account; each is independent and a failed | ||
| # account never blocks the others. | ||
| connected_count += await self._start_account_adapters( |
There was a problem hiding this comment.
Named accounts are invoked only after the default adapter succeeds. If a configured default bot has a bad token or transient connect failure, this branch is skipped and every healthy named bot is unavailable. Start named accounts independently of default-account connect outcome.
| ) | ||
| await self._safe_adapter_disconnect(adapter, platform) | ||
| continue | ||
| if not success: |
There was a problem hiding this comment.
Initial named-account connection failures are disconnected and then discarded here. The new reconnect watcher only processes _failed_account_adapters, but this path never enqueues one, so a transient startup failure for a named bot is never retried. Queue retryable initial failures with the same account-scoped backoff state.
|
All three findings were correct — the first is the important one, and you're right that the feature didn't actually work for normal traffic. Pushed fixes: 1. Account stamped on the real inbound path. You're exactly right: I stamped the two auth-helper sources but normal Telegram traffic builds its 2. Named accounts start independently of the default adapter. Moved 3. Initial account-connect failures now queue for reconnect. Retryable startup failures (connect returns False with a retryable adapter, or raises) enqueue into 4. On the CI failures (a separate self-inflicted bug the review surfaced): Re-verifying end to end and will confirm green. |
|
CI is green now (run 29688151750, all required checks pass). Two commits since the review: the four review fixes, then a bare-runner-safety follow-up CI caught — the account reconnect watcher and fatal-error handler are exercised by partially-constructed test runners (no |
edb45a7 to
72e434d
Compare
|
Rebased onto current main (resolves the conflicts from the god-file decomposition + the new transport-resolution layer). All 7 commits reapplied; 48/48 multi-account tests green, and the reconciled-area regression suites pass in isolation (platform_reconnect 56, discord_liveness 19, runner_fatal_adapter 7, delivery 33, queue_consumption 14, notice_rendering 9). Conflict reconciliations worth flagging for review:
|
|
Pushed The previous head rejected
Tests: 13 new (parsing forms, the six derivation cases, runner/shared equivalence, all three fail-closed paths). Regression run: 61/61 across the five multi-account suites, and 50/50 across the nine existing Still deliberately out of scope, same as before: per-account startup home broadcast, since main has since rewritten that path to be transport/relay-aware and folding it in here would put an unrelated rewrite in this diff. Happy to do it as a follow-up once this lands. |
|
CI note on
Everything else on Happy to send the optional-dependency guard for that test file as a separate one-file PR if it's not already being handled — I didn't find an existing report for it, but it's unrelated to this change and doesn't belong in this diff. |
980aa66 to
78cee12
Compare
|
Withdrawing the offer I made in my last comment — no separate PR is needed, that failure was already fixed on main and I should have checked the tip before offering.
Rebased onto Re-verified after the rebase rather than assuming 591 commits of drift were harmless: 61/61 across the five multi-account suites, 13/13 on the new per-account send tests, and the existing So this PR should now go fully green. The per-account |
SummaryTwo open PRs address #8287: #10455 adds Telegram account configuration, adapter startup, channel lookup, reconnect bookkeeping, and account-qualified sends, but does not add bot identity to session keys or consistently update delivery consumers; #67455 extends the account dimension through configuration, adapter registries, inbound identity, isolated sessions, delivery, cron, lifecycle recovery, home channels, and send-message routing. Related pull requests
Duplicates#10455 and #67455 substantially overlap on Telegram multi-account configuration, adapter startup, routing, reconnect behavior, and outbound sends; #67455 supersedes #10455 by also implementing account-aware session identity and the broader delivery/lifecycle architecture required by the contributor review. Suggested consolidationKeep #67455 open with a salvage path focused on renewed contributor review of the corrected inbound identity, independent startup, reconnect, delivery, cron, and send-message paths; it is the recorded best fix and preserves the contributor review's keep_open disposition. Close #10455 as a duplicate of #67455 despite its keep_open review: the complete diffs show that #67455 carries forward its useful configuration and routing work while adding the session isolation and consumer-wide account registry that the #10455 review required. Complex graphflowchart LR
classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
classDef best stroke-width:3px,stroke:#b45309
classDef target stroke-width:3px,stroke:#4338ca
I8287(["issue #8287 (open)"])
P67455["PR #67455 (open)"]
P67455 -->|best fix| I8287
class I8287 open
class P67455 open
class P67455 best
class P67455 target
click I8287 "https://github.com/NousResearch/hermes-agent/issues/8287"
click P67455 "https://github.com/NousResearch/hermes-agent/pull/67455"
Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label). Cross-PR triage: Reviewed 2 pull requests and 1 issue in this complex. Each diff was read against this issue; Assessment working set: 145 kB of PR diffs, 15 kB of issue/PR text, 18 kB of discussion (18 comments), 3 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch. |
78cee12 to
2fc8a1b
Compare
2fc8a1b to
6654e3b
Compare
|
Rebased onto current main ( Re-verified the premises rather than assuming they survived: the inbound stamp is still the single central site ( |
First slice of the account-aware gateway: configuration surface only, no runtime behavior change. - TELEGRAM_BOT_TOKEN_<ACCOUNT> env vars declare additional bot accounts (lowercased names); the unsuffixed TELEGRAM_BOT_TOKEN remains the default account, so single-bot setups parse byte-identically. Tokens are secrets: env/.env is their supported home. - platforms.telegram.accounts.<name> in config.yaml carries the behavioral per-account settings (display names, allowlists, home channels) and merges with env tokens on the account name; the block arrives top-level or bridged into extra (the same two-route pattern as gateway_restart_notification) and round-trips through to_dict. Registry, session-key, and routing slices follow in this branch per the acceptance architecture in the NousResearch#10455 review.
Second slice: the same chat reached through two bot accounts is two sessions. - SessionSource.account carries which bot received the message (stamped by the adapter in the upcoming registry slice); wire-invisible when unset, serialized like profile. - The account rides in the session-key NAMESPACE slot — the same mechanism profiles use: agent:main@support / agent:coder@support. Positional parsers keep their layout (parts[2] == platform), and single-bot gateways produce byte-identical keys (locked by test). - build_session_key reads the account from the SOURCE, not a caller parameter, so the adapter-level guard and the session store derive the same key for the same event — per-key guards diverging is the NousResearch#64934 bug class, and this keeps that door shut by construction. - The two namespace readers are account-aware via a shared helper: _profile_from_session_key strips the suffix instead of resolving 'main@support' as a profile name, and _parse_session_key accepts the suffixed default namespace (named-profile keys stay excluded). - Account names are charset-restricted at config parse ([a-z0-9][a-z0-9_-]*) so ':' and '@' can never reach a key. Includes the NousResearch#10455-review isolation test: same chat + user via two accounts yields distinct keys.
…ping (NousResearch#8287) Third slice: one adapter instance per named bot account. - _account_adapters[platform][name] mirrors the proven _profile_adapters two-level registry: self.adapters stays the default account's map, so every existing consumer is untouched when no named accounts exist. - _authorization_adapter gains the account dimension with the same fail-closed contract as profiles: a stamped account with no registry entry resolves to None rather than the default bot — replying out the wrong bot is worse than not replying. _adapter_for_source reads source.account, so inbound routing follows the stamp automatically. - Each account adapter sees an ordinary derived PlatformConfig (its own token, its own home_channel with the platform implicit, account-block settings overriding platform extra, the accounts map stripped) — adapter internals stay account-agnostic. - BasePlatformAdapter.account_name identifies the serving account; the Telegram adapter copies it onto every inbound SessionSource, which is where the per-account session keys from the previous slice light up. - Startup: named accounts start after the default adapter, each independent (a failed account never blocks the others). Accounts-only configurations (tokens with no default credential) skip the doomed token-less default connect and start named accounts directly. Per-account reconnect queueing, delivery/status/cron consumers, and setup UX land in the remaining slices.
…ousResearch#8287) Fourth slice: the outbound half and lifecycle recovery. - DeliveryTarget grows the account dimension: 'telegram@support:123' addresses a chat through the support bot, 'telegram@support' its home channel, and origin targets inherit the account the message arrived on — replies always leave through the bot that received them. - DeliveryRouter resolves account targets through the account registry fail-closed: account-addressed content never leaves through the default bot. Registry sync rides the existing three adapter-sync sites (shared references, so registrations propagate live). - Fatal errors: the stale-owner guard in _handle_adapter_fatal_error sees the DEFAULT adapter occupying the platform slot and would silently ignore a dying named account — no reconnect, no status, no log. Named accounts now take their own path: same contract, scoped to the account registry (stale-guard against their own slot, pop + disconnect, retryable failures queued under (platform, account)), and an account's death never touches the default slot or the all-platforms-down shutdown logic. - Reconnect watcher gains a named-account pass mirroring the platform pass (same capped exponential backoff); success re-registers into _account_adapters, never the platform slot. Adapter wiring is shared between account startup and account reconnect via _wire_account_adapter so the two can never drift. Remaining slice: cron/home-channel account targets in the runner's target-resolution paths, setup UX, and example-config docs.
…up UX (NousResearch#8287) Final slice: the remaining consumers from the NousResearch#10455 review checklist, plus the user-facing surface. - Cron 'deliver' strings accept named-account targets: telegram@support:123 routes through the support bot. The account splits off before platform validation (downstream maps know 'telegram', not 'telegram@support'), rides the resolved-target dict, and reaches DeliveryTarget(account=...). The delivery router syncs the gateway's account registry via the module-level runner weakref gateway/run.py already keeps for module consumers; with no gateway running (CLI cron), account targets fail closed with the router's 'No adapter configured' error. - Home-channel broadcasts (startup notice + shutdown notification) reach every account's own home channel: a shared _iter_live_adapters_with_home yields (platform, adapter, home) for default adapters (platform-level home) and account adapters (their derived config's home), preserving the snapshot-before-iterate shutdown lesson. - hermes gateway setup gains an optional multi-bot stanza after the home channel step: name-validated accounts, token prompts with the existing regex/retry loop, saved as TELEGRAM_BOT_TOKEN_<NAME> in .env. - cli-config.yaml.example documents platforms.telegram.accounts with the name charset, the env token convention, and the delivery syntax.
…count lifecycle (NousResearch#8287) Addresses the NousResearch#67455 review — three correct findings, plus the CI failures the review surfaced. - Account is now stamped in BasePlatformAdapter.build_source(), the single construction site every platform's NORMAL inbound event flows through. The prior stamps were only on Telegram auth-helper sources, so ordinary named-bot traffic kept account=None and routed through the default session key and default egress adapter — the feature silently no-op'd for real messages. Regression test added for this exact path. - Named-account adapters start independently of the default adapter's connect outcome (moved out of the 'if success' block): a bad or absent default token no longer keeps healthy named bots offline. - Retryable initial account-connect failures now queue for background reconnection via a shared _queue_account_reconnect helper (the same queue the fatal-error path and reconnect watcher use), so a transient startup blip is retried instead of dropped until the next restart. - hermes send rejects @account targets with an actionable error rather than an opaque 'Unknown platform'; per-account send routing is a scoped follow-up. CI-failure fix: a MagicMock/SimpleNamespace source auto-creates a truthy 'account' attribute (AGENTS.md pitfall NousResearch#17, already guarded for 'profile'), which tripped fail-closed account resolution. The account read is now isinstance-coerced to str-or-None in _authorization_adapter and build_session_key, so any source built without an explicit account reads as the default.
…8287) CI caught real regressions the account lifecycle introduced: the reconnect watcher and fatal-error handler are exercised by tests that build a partially-constructed GatewayRunner (no __init__), so they lack _account_adapters / _failed_account_adapters — and a MagicMock adapter auto-creates a truthy account_name (AGENTS.md pitfall NousResearch#17), routing a default adapter's fatal error down the named-account path. - Reconnect watcher iterates _failed_account_adapters via getattr, so a bare runner sees an empty account queue instead of AttributeError. - _handle_adapter_fatal_error isinstance-guards account_name: only a real str routes to the account fatal path; a Mock reads as the default adapter. - All three delivery_router.account_adapters sync lines and the account-fatal registry read use getattr defaults. Verified: the CI-failing suites (test_platform_reconnect, test_platform_reconnect_fd_leak, test_discord_liveness) plus the runner lifecycle files all green in isolation.
Closes the last account-blind consumer from teknium1's review: send_message rejected 'telegram@support:123', so the one path a reviewer flagged as incomplete stayed incomplete. send_message already builds its own PlatformConfig from gateway config rather than reaching a live adapter registry, so per-account send needs no new plumbing — just the account's config. The target's platform segment now splits into (platform, account) BEFORE any platform lookup (downstream maps and the Platform enum know 'telegram', not 'telegram@support'), and the named account's derived PlatformConfig replaces the platform's for the rest of the path: its own token, its own home channel, its own platform-extra overrides. 'telegram@support' with no chat reaches the SUPPORT bot's home channel, not the default bot's. The derivation is no longer duplicated: GatewayRunner._account_platform_config now delegates to gateway.config.derive_account_platform_config, shared with this send path, so account-adapter startup and send resolve an account's token/home/extra identically. resolve_platform_account() is the one place 'platform[@account]' is parsed, with '@default' and '@' both meaning the default account. Fails CLOSED on a bad account — unknown account, or one with no token — naming the configured accounts and the exact env var to set, rather than silently falling back to the default bot's credential and delivering to the wrong audience. Tool schema documents the '@account' target form. 13 tests: parsing (plain / named / '@default' / empty / empty-input), derivation (token override, accounts-map stripped, implicit home-channel platform, extra override, empty block, base config untouched), the runner-vs-shared equivalence, and all three fail-closed cases. Regression: 61/61 across the five multi-account suites and 50/50 across the nine existing send_message test files.
6654e3b to
10df63a
Compare
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
|
Splitting this into reviewable pieces rather than leaving it at 1,750 lines / 17 files. It's been open 26 days. Looking at the last 40 merged PRs in this repo, the median is 136 lines and every one of them merged inside 24h — so a PR this size was never going to get a fair read, regardless of whether the code is right. Triage recorded it as the First slice is up: #86497 — config parsing only, 46 lines of Keeping this open until the sequence supersedes it, so nothing is lost and the review history here stays linked. Happy to re-cut the boundaries if a different split reviews better — or to abandon the split and keep it here if you'd prefer the whole thing in one place. The three findings from your earlier review are all folded into the relevant slices, not dropped: |
…sResearch#8287) Third slice: one adapter instance per named bot account, and the inbound stamp that makes the previous slice's per-account session keys light up. - _account_adapters[platform][name] mirrors the proven _profile_adapters two-level registry: self.adapters stays the default account's map, so every existing self.adapters[...] site is untouched when no named accounts are configured (the dict is empty). - _authorization_adapter gains the account dimension with the same fail-closed contract as profiles: a stamped account with no registry entry resolves to None rather than the default bot — replying out the wrong bot is worse than not replying. _adapter_for_source reads source.account, so inbound routing follows the stamp automatically. A named account under a secondary profile is not a supported combination yet and also fails closed, checked before the active-profile fast path so it holds for a named ACTIVE profile too. - Each account adapter sees an ordinary derived PlatformConfig (its own token, its own home_channel with the platform implicit, account-block settings overriding platform extra) — adapter internals stay account-agnostic. The accounts map is stripped from the derived extra: an account must never be able to spawn accounts. - The account is stamped in BasePlatformAdapter.build_source(), the single construction site every platform's NORMAL inbound event flows through. Stamping only the Telegram auth helpers would leave ordinary named-bot traffic at account=None — default session key, default egress — i.e. the feature would silently no-op for real messages. Both Telegram auth-path helpers are stamped too, so the adapter-level guard and the session store cannot derive different keys for one event (the NousResearch#64934 bug class). Startup rides the parallel connect fan-out added by NousResearch#83791 rather than bolting a serial tail onto it. _prepare_account_adapters() creates and wires the account adapters, and their (platform, config, adapter) triples join the same _pending_connects list the default adapter uses, so named bots connect concurrently with every other platform — connecting them in a loop would reintroduce exactly the head-of-line blocking that PR removed, at N accounts x one timeout each. Consequences, all covered by tests that drive the real start(): - Named accounts no longer depend on the default adapter's outcome, so a bad or absent default token cannot keep healthy named bots offline (NousResearch#67455 review finding). - Registration happens in the single-threaded aggregation loop keyed off adapter.account_name, so shared state is mutated exactly as before. - A named account owns no entry in self.adapters and never claims the platform's single _failed_platforms retry slot — that slot would respawn the DEFAULT adapter from the account's config. - The platform runtime-status row reports the DEFAULT account's health, so an account connect neither flips it to "connecting" nor marks the platform down while the default bot is serving fine. - An accounts-only platform (named tokens, no default credential) skips the doomed token-less default connect entirely. Per-account reconnect queueing, delivery/status/cron consumers, and setup UX land in the remaining slices; a named account that fails to connect at startup is disconnected cleanly and retried at the next gateway start until then.
What does this PR do?
One gateway, N Telegram bot accounts, each with fully isolated sessions — closes the gap confirmed in the #10455 review ("the underlying gap remains on current main"), built to that review's acceptance architecture item by item. Claimed on #8287 before building.
The review's checklist → where each lands:
_account_adapters[platform][name], the account-dimension mirror of the proven_profile_adapterspattern:self.adaptersstays the default account's map, so the ~93 existing consumers are untouched when no named accounts exist. Consumers updated together in this PR: adapter resolution (_authorization_adapter/_adapter_for_source, fail-closed like profiles — a stamped account with no live adapter never falls back to the default bot), outbound delivery (DeliveryTarget.account, router resolves account targets through the registry fail-closed), fatal-error handling (named accounts take a mirrored path — without it the existing stale-owner guard silently ignores a dying account, since the default adapter occupies the platform slot), background reconnection (a named-account pass with the same capped backoff; success re-registers into the account registry, never the platform slot), per-account runtime status (telegram@supportkeys), and home-channel broadcasts (startup/shutdown notices reach each account's own home).deliverstrings accepttelegram@support:123456(account splits off before platform validation and rides the resolved target intoDeliveryTarget); the cron-side router syncs the gateway's account registry through the module-level runner weakrefgateway/run.pyalready maintains; broadcasts iterate default + account adapters via one shared helper.BasePlatformAdapter.account_name, stamped by the gateway at account-adapter startup and copied by the Telegram adapter onto every inboundSessionSource.account. The account rides the session-key namespace slot (agent:main@support:telegram:…) — the same mechanism profiles use — so every positional key parser keeps its layout, and single-bot gateways produce byte-identical keys (locked by test).build_session_keyreads the account from the source, not a caller parameter, so the adapter guard and the session store can never derive different keys for the same event (the Two turns can run concurrently on one gateway session: interleaved transcript flushes, permanent alternation wedge, repair_message_sequence fires on every request #64934 lesson, applied by construction).test_same_chat_two_bots_two_sessions— same chat + same user via three accounts → three distinct sessions — plus 45 more behavior tests across config, sessions, adapters, delivery, and cron targets.Config surface (policy-clean): tokens are secrets →
TELEGRAM_BOT_TOKEN_<ACCOUNT>in.env(the convention #10455 introduced, kept); behavioral settings →platforms.telegram.accounts.<name>in config.yaml (display name, per-accounthome_channelwith the platform implicit, any platform-extra override); account names charset-restricted ([a-z0-9][a-z0-9_-]*) so:/@can never reach a session key;hermes gateway setupgains an optional multi-bot stanza in the existing prompt style;cli-config.yaml.exampledocuments the block. Accounts-only configs (tokens but no default credential) start named accounts directly instead of queueing a doomed token-less default connect.Relationship to #10455 (@yimwoo): that PR pioneered the ask and its env-token convention, and its review produced the acceptance architecture this PR implements. The architectural rework the review required (synthetic
telegram@accountplatform keys → an account dimension on the existing registries) meant no commits survive cherry-pick — credit here is for the convention and for proving the demand; happy to addCo-authored-byif maintainers prefer it for the convention carry-over.Deliberate limits (flagged, not hidden): named accounts inside secondary multiplexed profiles fail closed (unsupported combination — resolving it needs a product call on precedence); bare
telegram@supportcron home-targets (account home channel without explicit chat) are deferred — cron home resolution is env-var-based today and the account's home lives in its derived config (follow-up if wanted); this PR is Telegram-scoped per the issue, but nothing in the registry, session-key, delivery, or lifecycle layers is Telegram-specific — a second platform adopts it by stampingaccount_name.Related Issue
Closes #8287. Unblocks the session-identity prerequisite shared by #10143 (topic→profile routing) and #21587 (guest bots).
Type of Change
Changes Made
gateway/config.py—accountsblock parsing (two-route likegateway_restart_notification),TELEGRAM_BOT_TOKEN_<ACCOUNT>env scan, name-charset validationgateway/session.py—SessionSource.account(+serialization), account in the key namespace slot,split_key_namespaceused by both namespace readersgateway/run.py— account registry + derived per-accountPlatformConfig, account-adapter startup (incl. accounts-only path) and shared wiring, account fatal-error path + reconnect pass,_parse_session_keyaccount tolerance, home-broadcast iteration helpergateway/authz_mixin.py— account dimension in adapter resolution, fail-closedgateway/platforms/base.py—account_nameadapter identityplugins/platforms/telegram/adapter.py— inboundSessionSource.accountstamping (both construction sites)gateway/delivery.py—DeliveryTarget.account,@accountparse/round-trip, fail-closed router resolutioncron/scheduler.py—@accountdeliver-string parsing + registry sync for cron deliveryhermes_cli/setup.py— optional multi-bot setup stanzacli-config.yaml.example—accountsdocumentation blocktests/gateway/test_telegram_multi_account_{config,sessions,adapters,delivery,targets}.pyHow to Test
scripts/run_tests.sh tests/gateway/test_telegram_multi_account_config.py tests/gateway/test_telegram_multi_account_sessions.py tests/gateway/test_telegram_multi_account_adapters.py tests/gateway/test_telegram_multi_account_delivery.py tests/gateway/test_telegram_multi_account_targets.py— 46/46.tests/cron/test_scheduler.py(222/222), delivery/DM-topics/turn-lease/conversation-scope suites (84/84) — all green post-rebase on current main.TELEGRAM_BOT_TOKEN_SUPPORTalongsideTELEGRAM_BOT_TOKEN, start the gateway, message both bots in the same chat → two isolated sessions (keysagent:main:telegram:…andagent:main@support:telegram:…); reply threads out the receiving bot;cronjob(..., deliver="telegram@support:<chat>")delivers via the support bot.Checklist
Code
Documentation & Housekeeping
cli-config.yaml.exampleblock added; setup wizard prompt addedcli-config.yaml.exampleupdated — yes (accounts block)