Skip to content

feat(gateway): per-account session identity (#8287, 2/6) - #86532

Open
Hotragn wants to merge 2 commits into
NousResearch:mainfrom
Hotragn:feat/8287-02-session-identity
Open

feat(gateway): per-account session identity (#8287, 2/6)#86532
Hotragn wants to merge 2 commits into
NousResearch:mainfrom
Hotragn:feat/8287-02-session-identity

Conversation

@Hotragn

@Hotragn Hotragn commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Second slice of the #67455 split (#8287, triage-recorded best fix). Follows #86497.

Stacks on 1/6. GitHub won't let a fork PR target another fork branch, so this contains #86497's commit as well as its own. Once #86497 merges this shrinks to its own ~190 lines automatically — review just the second commit (per-account session identity) if 1/6 is still open.

This slice alone 191 lines, 3 files
Cumulative with 1/6 359 lines, 5 files

What it does

Makes the account part of session identity, so two bots sharing a chat id don't collide.

SessionSource carries an account field, and the session key derivation folds it in. Without this, a message to the support bot and a message to the sales bot in the same Telegram chat resolve to the same session — they'd share transcript history and interleave turns. That's the same class of many-to-one key collision as #64934, just reached through a different door.

account is None for the default/single-bot adapter, and the derived key is byte-identical to today in that case. Single-bot deployments are unaffected.

Why it isn't independently useful yet

Nothing populates source.account until 3/6 (adapter registry + build_source() stamping) — so on its own this widens the identity contract without exercising it. Same disclosure as 1/6: I'd rather state it than have it found in review.

Worth flagging that 3/6 is where your earlier review finding lands — the original PR stamped account only on Telegram's auth-helper paths, so ordinary inbound traffic kept account=None and the whole feature silently no-op'd for real messages. The stamp moves into BasePlatformAdapter.build_source(), the single construction site every platform's normal event flows through.

Sequence

1/6 config parsing (#86497) → 2/6 session identity (this) → 3/6 adapter registry + inbound stamping → 4/6 delivery routing + reconnect → 5/6 per-account send_message → 6/6 cron targets, home broadcasts, setup UX.

Tests

tests/gateway/test_telegram_multi_account_sessions.py — 9 tests: account folded into the key, distinct accounts in one chat get distinct sessions, account=None reproduces the current key byte-for-byte, and round-tripping through SessionSource.

17 passed across both slices' test files. Full tests/gateway/ run: the sorted set of erroring modules is identical to pristine upstream/main (13 either way — pre-existing optional-dep gaps on my Windows box). Compared by test-id set rather than count, since collection aborts early and raw counts drift between runs.

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.
@alt-glitch alt-glitch added type/feature New feature or request comp/gateway Gateway runner, session dispatch, delivery platform/telegram Telegram bot adapter area/config Config system, migrations, profiles P3 Low — cosmetic, nice to have sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Aug 15, 2026
@Enough1122

Copy link
Copy Markdown
Contributor

AI code review — automated review for reference, author can ignore or act on any point.

feat(gateway): per-account session identity (#8287, 2/6)

  1. Profile + account gap: build_session_key(source(account="support"), profile="coder") now generates agent:coder@support:telegram:..., but gateway/run.py::_parse_session_key only accepts the main namespace (with optional account suffix) — agent:coder@support:... parses to _ns == "coder" and returns None. _parse_session_key is the routing fallback when no cached SessionSource exists (run.py:9675, 22731, 23170 — delivery dedup, process-event source derivation). So a named-profile + multi-account session can silently fail to route/enrich through those fallbacks. Either accept any agent:<ns>[@account] here, or document that named-profile + multi-account is out of scope for this parser — and add a test for agent:coder@support:... either way.

  2. Account name "default" asymmetry: _session_key_namespace treats account == "default" as no account (byte-identical legacy keys), but gateway/config.py will happily register an account literally named default (from TELEGRAM_BOT_TOKEN_DEFAULT or a YAML accounts: {default: ...} block). The result is a dead accounts block that never affects session keys. Consider rejecting default as an account name at parse time.

  3. Minor: the env-var enumeration (for _env_name in sorted(os.environ)) reads the raw process env and relies on .env having been loaded before load_gateway_config runs. If any gateway entry path skips dotenv loading, TELEGRAM_BOT_TOKEN_<ACCOUNT> tokens would be silently invisible. Worth confirming all gateway startup paths load .env before this loop (the comment asserts dotenv loads there).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have platform/telegram Telegram bot adapter sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants