Skip to content

Telegram: group mentions start a new session per bot message; key groups chat-wide like private chats #874

Open

Description

@Lukavyi

Bug

A Telegram bot in a group has no memory between mentions. Every @bot mention that is not a reply to a bot message starts a new session with one message of context.

Cause: group and supergroup inbound messages key the session to a bot message id (telegram:<chatId>:<topicId>:<botMessageId>, see conversationIdForMessage in telegram/state.ts). Only a reply-chain to one specific bot message keeps continuity. Private chats already key chat-wide (telegram:<chatId>:<topicId>:) since #377; groups are the only surface that resets.

Timeline in one group:

  1. U2: @bot question A → session chat::101
  2. U2 replies to the bot's answer → same session, fine
  3. U1: @bot question B (fresh mention, not a reply) → new session, blind to A
  4. U2 replies to the old answer A a day later → resumes chat::101, blind to everything since

Fix

Key groups the same way as private chats: one session per chat, or per forum topic. Mentions, replies to bot messages, callback queries and outbound sends all resume it.

  • PR [1/2] feat(eve)!: key Telegram group sessions chat-wide #3020 does exactly this. It also treats message_thread_id as a session discriminator only when is_topic_message is set, because non-forum supergroups stamp reply-chain ids on replies and those must not fragment the chat-wide session.
  • Per-bot-message anchoring is removed rather than kept behind an option: it stays reproducible via conversationId on a proactive receive target (and Let the host pick the session for an inbound turn (continuation token override) #671 would cover inbound), while the reverse was not possible without patching channel internals. Slack and Teams already key sessions to the platform's visible container (a thread); see the channel comparison in the PR.

We have run chat-wide keying as a patchedDependencies patch in production on two group bots since July.

Follow-up, separate PR

Unaddressed group messages are dropped by the dispatch gate and reach the session nowhere, so even with one chat-wide session the bot does not see what people said between mentions. Slack has threadContext for this. The plan is an observe result from onMessage that appends a message to the session as history without running a turn; that touches core delivery and will come as a stacked PR after #3020.

Related

#364 / #377 (where group anchoring was kept), #671 (host-controlled continuation token), #897 (fixed, hit while building the observe buffer), #211 (same class of bug in Teams, fixed as a bug), #223 (Slack: respond without mentions).

Original report (long version with diagrams, proposed API shape and edge cases)

What problem are you trying to solve?

I run a coach-style agent in a Telegram group. The group has an ongoing human conversation, and the bot is mentioned occasionally ("@bot, what do you think?"). Expected behavior for this product shape: the bot answers with awareness of the conversation so far. Actual behavior: every fresh mention starts a session whose context is that single message.

Two eve behaviors combine into this:

  1. Session keying anchors groups to a bot message id. The continuation token is telegram:<chatId>:<messageThreadId>:<conversationId>, and for group/supergroup chats conversationId is anchored to the bot's posted message id (behavior documented and deliberately preserved in Telegram: proactive private-chat message re-keys session, breaking continuity #364 / fix(eve): telegram - preserve proactive private chat sessions #377: "Group and supergroup chats still need bot-message anchors so replies to different bot messages can resume different sessions in the same chat"). A mention that is not a reply to a bot message resolves to a fresh conversation anchor, so it lands in a brand-new session. Only a reply-chain to one specific bot message keeps continuity.

  2. Unaddressed group messages are dropped without a trace. The dispatch gate correctly skips messages that don't address the bot, but there is no hook to retain them as history. So even if all mentions collapsed into one chat-wide session, the human-to-human conversation between mentions would still be invisible to the agent.

Here is the same group-chat timeline mapped to sessions, today vs. desired.

Today — a fresh mention mints a new session; only reply-chains to a specific bot message keep continuity; everything unaddressed vanishes:

flowchart LR
    subgraph chat["Group chat (chronological)"]
        direction TB
        m1["U1: talks about their week (unaddressed)"]
        m2["U2: @bot question A"]
        b1["bot: answer A (message 101)"]
        m3["U2: reply to 101 - follow-up"]
        m4["U1 and U2 discuss it (unaddressed)"]
        m5["U1: @bot question B (fresh mention, not a reply)"]
        b2["bot: answer B"]
        m6["U2: much later - replies to old answer A (message 101)"]
        m1 --> m2 --> b1 --> m3 --> m4 --> m5 --> b2 --> m6
    end

    subgraph sA["Session telegram:chat::101"]
        sA1["question A + its reply chain"]
        sA2["late reply resumes this old session - it never saw question B or anything after answer A"]
    end
    subgraph sB["Session telegram:chat::new - starts with 1 message of context"]
        sB1["question B, blind to everything above"]
    end
    subgraph gone["Dropped by dispatch gate - retained nowhere"]
        g1["all unaddressed messages"]
    end

    m2 --> sA
    m3 --> sA
    m5 --> sB
    m6 --> sA2
    m1 --> gone
    m4 --> gone
Loading

Desired — one chat/topic-wide session; addressed messages run turns, unaddressed ones are appended as history without triggering a turn:

flowchart LR
    subgraph chat2["Group chat (chronological)"]
        direction TB
        n1["U1: talks about their week (unaddressed)"]
        n2["U2: @bot question A"]
        nb1["bot: answer A"]
        n3["U2: reply - follow-up"]
        n4["U1 and U2 discuss it (unaddressed)"]
        n5["U1: @bot question B"]
        nb2["bot: answer B"]
        n6["U2: much later - replies to old answer A"]
        n1 --> n2 --> nb1 --> n3 --> n4 --> n5 --> nb2 --> n6
    end

    subgraph sC["One session telegram:chat:: (chat/topic-wide)"]
        direction TB
        c1["history: U1 week notes"]
        c2["turn: question A -> answer A"]
        c3["turn: follow-up"]
        c4["history: U1/U2 discussion"]
        c5["turn: question B - answered with full awareness of the conversation"]
        c6["turn: late reply to answer A - reply_to says which message it's about, session still has the whole story"]
        c1 --> c2 --> c3 --> c4 --> c5 --> c6
    end

    n1 -. "context: 'all' (no turn)" .-> c1
    n2 --> c2
    n3 --> c3
    n4 -. "context: 'all' (no turn)" .-> c4
    n5 --> c5
    n6 --> c6
Loading

The per-bot-message anchoring is a reasonable default for "many parallel threads with the bot in one group". But for an assistant that participates in one continuous group conversation, it makes the group surface effectively memoryless, and there is currently no supported way to opt out:

Proposed solution

Any of these would solve it; ordered by preference:

  1. Make chat-wide keying the group behavior. Group/supergroup inbound messages and outbound sends resolve to the chat/topic-wide token (telegram:<chatId>:<messageThreadId>:), exactly like private chats do after fix(eve): telegram - preserve proactive private chat sessions #377, so every mention resumes one ongoing session per group/topic. This covers both plain groups (no forum topics: empty messageThreadId → one session for the whole chat) and forum supergroups (each topic keeps its own messageThreadId → one session per topic), mirroring how Telegram: proactive private-chat message re-keys session, breaking continuity #364 defines "chat-wide" as chat/topic-wide. I'd propose removing automatic bot-message anchoring outright rather than keeping it behind a config flag (e.g. a groupSessionMode option):

    • It's consistent with how every other surface behaves after fix(eve): telegram - preserve proactive private chat sessions #377 — private chats and forum topics already get one continuous session; groups are the only surface that resets on every fresh mention.
    • Bot-message anchoring isn't needed for correctness even in the "bot posts many items" scenario: with chat-wide keying, a reply to a specific bot message still carries reply_to context, and the session history contains the original item, so the model can disambiguate. What anchoring actually buys is isolation — parallel turns on separate tokens (relevant to Serialize concurrent deliveries to one continuation token #624), smaller per-thread context, no cross-talk between concurrent conversations. That's a real optimization for high-traffic digest/alert bots, but it's the specialized case, and a config flag is a heavy way to serve it.
    • The expressiveness is asymmetric, so nothing is lost: with chat-wide keying as the base behavior, today's anchoring remains fully reproducible in userland — via Let the host pick the session for an inbound turn (continuation token override) #671's resolveContinuationToken hook ("if the inbound is a reply to a bot message, return telegram:<chatId>:<threadId>:<repliedBotMessageId>, else null"), via explicit target.conversationId on proactive sends (already an honored contract per Telegram: proactive private-chat message re-keys session, breaking continuity #364), or simply via forum topics. The reverse is not true: with bot-message anchoring as the base, one continuous group conversation cannot be assembled without patching channel internals. One default behavior plus one escape hatch beats two modes.
    • Anchoring also produces wrong context on its own terms: a late reply to an old bot answer resumes that old anchored session, which is blind to everything that happened in the chat since (see the first diagram above). So the behavior being removed isn't just niche — it's subtly incorrect even for the conversations it was designed for.

    This is a behavior change for existing group bots, so it presumably rides a major version; if an interim opt-in is wanted before that, the Let the host pick the session for an inbound turn (continuation token override) #671 hook alone is enough to get chat-wide keying today.

  2. Host-controlled continuation token — Let the host pick the session for an inbound turn (continuation token override) #671's resolveContinuationToken hook. It's the general mechanism; chat-wide group sessions are then a two-line hook. If that lands, this issue only needs a docs example.

  3. A hook for non-dispatched group messages — a way to observe messages the dispatch gate skips (or an option to append them to the session as context without triggering a turn), so the agent can see the conversation that happened between mentions. This is complementary to 1/2: keying fixes session continuity, this fixes conversation visibility.

Zooming out, 1+3 generalize into a small group engagement policy with two independent axes — what the bot responds to vs what it reads:

telegramChannel({
  group: {
    respondTo: 'addressed' | 'all',
    // 'addressed' (today's behavior): reply only on @-mention or a reply to a bot message
    // 'all': run a turn on every group message (assistant fully participates)

    context: 'addressed' | 'all',
    // 'addressed' (today's behavior): only dispatched messages ever reach the session
    // 'all': non-addressed messages are appended to the session as history
    //        without triggering a turn, so the next mention sees the whole conversation
  }
})

respondTo: 'all' and context: 'all' both require Telegram privacy mode to be disabled for the bot (BotFather /setprivacy) or the bot to be a group admin — otherwise Telegram never delivers unaddressed messages to the webhook; worth a docs note rather than runtime magic. The same respondTo: 'all' request already exists for the Slack channel in #223, so the policy shape could be shared across channels.

Topic and migration edge cases the keying must handle

Whatever shape lands, chat-wide keying has four edge cases worth speccing up front:

  1. message_thread_id in non-forum supergroups is not a topic. Per the Bot API, Message.message_thread_id is the "unique identifier of a message thread or forum topic" and exists "for supergroups and private chats only". In a supergroup without forum mode, replies still carry it — it's the id of the reply chain, not a topic (and since every growing group is silently upgraded to a supergroup, this is the common case, while basic groups never carry the field at all). If the token naively includes every message_thread_id, "one session per chat" silently fragments into one-session-per-reply-chain. Only treat message_thread_id as a session discriminator when the chat is a forum (chat.is_forum) and the update has is_topic_message: true; otherwise key chat-wide.
  2. Forum supergroups: one session per topic, General included. In a forum, each topic gets its own token via messageThreadId; messages in the General topic carry no message_thread_id and should resolve to the chat-wide token — which conveniently makes General behave like a plain group.
  3. group → supergroup migration changes chat.id. Telegram delivers migrate_to_chat_id / migrate_from_chat_id service messages when a group upgrades. Since the continuation token embeds chatId, the existing session is orphaned and the conversation silently resets. The channel should treat the migration update as a re-key: alias the old token to the new chat id (or expose the migration to the host so it can).
  4. Toggling forum mode on/off must not brick existing sessions. Turning topics on: the General topic keeps resolving to the old chat-wide token, so history survives; new topics start fresh sessions — expected. Turning topics off: per-topic sessions become unreachable (their messageThreadId never appears again) and everything falls back to the chat-wide token. That degradation is acceptable, but it should be deliberate and documented — and it's another argument for keeping the chat-wide token as the stable base identity rather than something minted per bot message.

Current workaround

Keeping an app-side buffer of group messages via a custom onMessage and pasting recent history into the prompt on each mention — which duplicates chat persistence eve already has, and has the footguns of replacing default channel events (#623).

Related

Activity

  1. Lukavyi commented on Jul 17, 2026

    @Lukavyi
    Author

    Field report: we shipped this proposal as a patchedDependencies patch on eve 0.22.3 (chat-wide group keying incl. callback queries and outbound sends, plus an observe result from onMessage that buffers unaddressed group messages in the parked driver loop and flushes them ahead of the next addressed turn — no external storage). Running in production on two bots now; the acceptance scenario from the issue (untagged question → separate mention → answer with that context; later mention → remembers its own previous answer) passes.

    Two things we hit that are relevant to anyone implementing this upstream:

    1. coalesceDeliverPayloads loses batched messages — last-key-wins for message/context, so flushing buffered messages together with the addressed one delivered only the last payload's text. Filed separately as coalesceDeliverPayloads silently drops earlier messages in a batched delivery (last-key-wins for message/context) #897 since it reproduces in unpatched eve with any coalesced delivery batch.

    2. Parked sessions pin their creation deployment. The session's workflowEntry run keeps executing the deployment it was created on (only turnWorkflow children route to latest in production), so keying/driver-loop changes never reach existing sessions — we had to cancel the parked runs via @workflow/cli (workflow inspect runs --status running → workflow cancel <wrun>) to migrate. Worth documenting alongside whatever ships here, since changing group keying inherently orphans live group sessions.

  2. Lukavyi commented on Jul 27, 2026

    @Lukavyi
    Author

    @AndrewBarba any chance this one gets looked at? Or should I make a PR with a fix to eve?

  3. Lukavyi commented on Sep 4, 2026

    @Lukavyi
    Author

    @benpankow @ruiconti @AndrewBarba — one concrete question so this can move:

    Today conversationIdForMessage (packages/eve/src/public/channels/telegram/state.ts) keys every group mention that isn't a reply-to-bot to a fresh session. Result: a bot in a group has no memory between mentions. Private chats already key chat-wide since #377; groups are the only surface that resets.

    The fix is ~10 lines: for group/supergroup return null unless the inbound is a reply to a bot message (or drop the anchor entirely and let hosts opt back in via target.conversationId). We run exactly this as a patchedDependencies patch in production on two bots since July.

    Question: which of these do you want as a PR?

    I'll open the PR within a day of an answer. The PR checklist requires "an issue with prior discussion confirming this change is wanted", so I need one of you to pick before I send it.

  4. Lukavyi commented on Sep 4, 2026

    @Lukavyi
    Author

    PR with the fix: #3020 (chat-wide keying, forum topics kept, reply-chain thread ids in non-forum supergroups ignored). Draft until a maintainer weighs in.

    One more angle on why the group anchoring is the odd one out, from the other channels' keying:

    Channel Session key Boundary
    Slack channel:threadTs a thread, visible to the user
    Teams tenant:conversation:rootActivityId a thread, visible to the user
    Discord channel:interactionId one slash command; the channel has no inbound messages
    Telegram (before) chat:topic:botMessageId a reply chain to one bot message; not visible to the user
    Telegram (after) chat:topic: the chat or forum topic, visible to the user

    Slack and Teams key a session to the smallest container the platform itself shows: a thread. Telegram has no threads outside forum topics, and instead of taking the nearest visible container (chat or topic) the channel synthesized its own boundary from the reply chain to a bot message. Users cannot see that boundary, which is why a plain @bot mention that is not a reply reads as "the same conversation" to them and as "a new session" to eve. Teams had the same class of bug in #211 (a follow-up mention in a thread lost context) and it was fixed as a bug, not made an option.

    The Telegram and Discord channels share one shape from the initial commit (conversationId state, anchor() after outbound posts, the same conversationId/initialMessage guard). Discord is interaction-only, so "one session per bot post" is the natural unit there. Telegram inherited that unit for groups; private chats were exempted from the start, and #377 later kept the group anchoring as-is while fixing proactive private chats. After this change Telegram behaves like Slack and Teams, and Discord stays the justified exception.

  5. changed the title [-]Telegram: fresh group mentions always start a new session — no supported way to keep one chat-wide conversation per group[/-] [+]Telegram: group mentions start a new session per bot message; key groups chat-wide like private chats[/+] on Sep 4, 2026
  6. Lukavyi commented on Sep 4, 2026

    @Lukavyi
    Author

    Both PRs are out of draft and ready for review:

    cc @benpankow @ruiconti, based on recent Telegram work and #897. GitHub does not let a fork request reviewers, so this mention is the request.

  7. roboeve commented on Sep 28, 2026

    @roboeve
    Contributor

    Confirmed on origin/main (e574dd3): two fresh Telegram group mentions (message IDs 101 and 102, neither replying to the bot) produce different continuation tokens (-100123::101 vs. -100123::102), so the second mention starts a new session.

    Repro: pnpm --filter eve exec vitest run --config vitest.unit.config.ts --dir /workspace/.repro /workspace/.repro/src/issue-874.test.ts (fails with the tokens above).

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions