Telegram: group mentions start a new session per bot message; key groups chat-wide like private chats #874
Description
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:
- U2:
@botquestion A → sessionchat::101 - U2 replies to the bot's answer → same session, fine
- U1:
@botquestion B (fresh mention, not a reply) → new session, blind to A - 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_idas a session discriminator only whenis_topic_messageis 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
conversationIdon a proactivereceivetarget (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:
-
Session keying anchors groups to a bot message id. The continuation token is
telegram:<chatId>:<messageThreadId>:<conversationId>, and for group/supergroup chatsconversationIdis 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. -
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
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
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:
session.setContinuationToken()only works on a live session mid-step (same limitation described in Let the host pick the session for an inbound turn (continuation token override) #671).- Writing a custom channel means re-implementing the Telegram dispatch/send stack, since
buildTelegramHandleisn't exported (also noted in Let the host pick the session for an inbound turn (continuation token override) #671). - Patching the token resolution via
pnpm patchedDependenciesworks (the Let the host pick the session for an inbound turn (continuation token override) #671 author runs this in production) but is a fork of channel internals.
Proposed solution
Any of these would solve it; ordered by preference:
-
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: emptymessageThreadId→ one session for the whole chat) and forum supergroups (each topic keeps its ownmessageThreadId→ 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. agroupSessionModeoption):- 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_tocontext, 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
resolveContinuationTokenhook ("if the inbound is a reply to a bot message, returntelegram:<chatId>:<threadId>:<repliedBotMessageId>, else null"), via explicittarget.conversationIdon 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.
-
Host-controlled continuation token — Let the host pick the session for an inbound turn (continuation token override) #671's
resolveContinuationTokenhook. 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. -
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:
message_thread_idin non-forum supergroups is not a topic. Per the Bot API,Message.message_thread_idis 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 everymessage_thread_id, "one session per chat" silently fragments into one-session-per-reply-chain. Only treatmessage_thread_idas a session discriminator when the chat is a forum (chat.is_forum) and the update hasis_topic_message: true; otherwise key chat-wide.- 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 nomessage_thread_idand should resolve to the chat-wide token — which conveniently makes General behave like a plain group. - group → supergroup migration changes
chat.id. Telegram deliversmigrate_to_chat_id/migrate_from_chat_idservice messages when a group upgrades. Since the continuation token embedschatId, 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). - 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
messageThreadIdnever 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
- Telegram: proactive private-chat message re-keys session, breaking continuity #364 / fix(eve): telegram - preserve proactive private chat sessions #377 — where the group anchoring behavior is documented and preserved
- Let the host pick the session for an inbound turn (continuation token override) #671 — continuation token override (mechanism that would cover option 2)
- Support bootstrapping sessions from external chat transcripts #91 — bootstrapping sessions from external transcripts (adjacent to option 3)
- Teams Channel - Calls from a Channel thread lose the context from earlier messages in the same thread #211 — the same "loses context from earlier messages in the same thread" problem class, fixed for the Teams channel
Activity
Lukavyi commentedon Jul 17, 2026
Field report: we shipped this proposal as a
patchedDependenciespatch on eve 0.22.3 (chat-wide group keying incl. callback queries and outbound sends, plus anobserveresult fromonMessagethat 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:
coalesceDeliverPayloadsloses batched messages — last-key-wins formessage/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.Parked sessions pin their creation deployment. The session's
workflowEntryrun keeps executing the deployment it was created on (onlyturnWorkflowchildren route tolatestin 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.Lukavyi commentedon Jul 27, 2026
@AndrewBarba any chance this one gets looked at? Or should I make a PR with a fix to eve?
Lukavyi commentedon Sep 4, 2026
@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
nullunless the inbound is a reply to a bot message (or drop the anchor entirely and let hosts opt back in viatarget.conversationId). We run exactly this as apatchedDependenciespatch in production on two bots since July.Question: which of these do you want as a PR?
minorchangeset)groupSessionMode: 'chat' | 'message'option ontelegramChannel(), default unchangedresolveContinuationTokenhook, no keying changeI'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.
feat(eve)!: key Telegram group sessions chat-wide
feat(eve)!: key Telegram group sessions chat-wide
feat(eve)!: key Telegram group sessions chat-wide
Lukavyi commentedon Sep 4, 2026
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:threadTstenant:conversation:rootActivityIdchannel:interactionIdchat:topic:botMessageIdchat:topic: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
@botmention 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 (
conversationIdstate,anchor()after outbound posts, the sameconversationId/initialMessageguard). 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.[-]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[/+]feat(eve)!: key Telegram group sessions chat-wide
feat(eve): observe channel deliveries as session history
feat(eve)!: key Telegram group sessions chat-wide
feat(eve): observe channel deliveries as session history
Lukavyi commentedon Sep 4, 2026
Both PRs are out of draft and ready for review:
[1/2] feat(eve)!: key Telegram group sessions chat-wide— one session per chat or forum topic;message_thread_idcounts only withis_topic_message, so reply chains in non-forum supergroups and in a forum's General topic (which Bot API delivers withoutis_topic_message, see Bot API Server is sending wrongmessage_thread_idtdlib/telegram-bot-api#447) do not split the session. Also restores the reply-to-bot text that feat(eve)!: unify public session operation APIs #1597 dropped from the delivery.[2/2] feat(eve): observe channel deliveries as session history—observe: trueon a delivery appends it as history without a turn; Telegram exposes it on theonMessageresult. Stacked on [1/2] feat(eve)!: key Telegram group sessions chat-wide #3020.cc @benpankow @ruiconti, based on recent Telegram work and #897. GitHub does not let a fork request reviewers, so this mention is the request.
roboeve commentedon Sep 28, 2026
Confirmed on
origin/main(e574dd3): two fresh Telegram group mentions (message IDs101and102, neither replying to the bot) produce different continuation tokens (-100123::101vs.-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).