Skip to content

[1/2] feat(eve)!: key Telegram group sessions chat-wide - #3020

Open
Lukavyi wants to merge 2 commits into
vercel:mainfrom
Lukavyi:telegram-group-session-mode
Open

[1/2] feat(eve)!: key Telegram group sessions chat-wide#3020
Lukavyi wants to merge 2 commits into
vercel:mainfrom
Lukavyi:telegram-group-session-mode

Conversation

@Lukavyi

@Lukavyi Lukavyi commented Sep 4, 2026

Copy link
Copy Markdown

Summary

A Telegram bot in a group has no memory between mentions. Group and supergroup inbound messages key the session to the bot message being replied to, or to the mention itself when it is not a reply (conversationIdForMessage in telegram/state.ts), and outbound group sends re-key the session to the posted message id. Every fresh @bot mention therefore starts a new session with one message of context, while private chats already keep one continuous session per chat since #377.

This makes groups behave like private chats: one session per chat, or per forum topic, keyed to telegram:<chatId>:<messageThreadId>:. Mentions, replies to bot messages, HITL callback queries, and outbound sends all resume that session, so the bot remembers what it said earlier in the chat.

  • Non-forum supergroups stamp message_thread_id on replies (the reply-chain root, not a topic). The thread id now counts as a session discriminator only when Telegram marks the message with is_topic_message, so a reply chain does not fragment the chat-wide session. isTopicMessage is exposed on TelegramMessage and TelegramMessageReference for this.

  • The per-bot-message anchoring is removed rather than kept behind an option, following the reasoning in Telegram: group mentions start a new session per bot message; key groups chat-wide like private chats #874: it is reproducible in userland (conversationId on a proactive receive target still pins a thread, 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. A late reply to an old bot message also resumed a session blind to everything since, so the anchoring was not correct even for the multi-thread case it served.

  • Second commit: a reply to a bot message is delivered with its text as message alongside the synthetic telegram_reply:<id> response again. The initial channel did this; feat(eve)!: unify public session operation APIs #1597 switched the reply path to respond() with only the response, so a reply that was not an answer to a pending ForceReply prompt resolved to nothing in the adapter and the session re-parked without a turn. With chat-wide sessions a reply to the bot is the ordinary follow-up, so this matters more than before. The adapter already had the fallback and the test for it.

Behavior change for existing group bots, hence the minor changeset and the ! in the commit. Existing parked group sessions keep their old anchored tokens; new mentions start on the chat-wide token.

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

Related to #874 (option 1 there). #671 is the more general mechanism; this is the smaller change that fixes the group use case now.

How this lines up with the other channels

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.

Validation

pnpm --filter eve exec vitest run --config vitest.unit.config.ts src/public/channels/telegram   # 63 passed
pnpm --filter eve exec tsc -p tsconfig.json --noEmit
pnpm run docs:check
pnpm run guard:invariants

New coverage: a fresh mention, a reply to a bot message (freeform HITL path), a mention inside a forum topic (is_topic_message: true keeps the topic), a reply-chain message_thread_id in a non-forum supergroup (stays chat-wide), a HITL callback query, and an outbound post that must not re-key the session. The existing anchoring tests were rewritten to assert chat-wide tokens; the one that asserted re-keying to the posted message id was dropped, and the explicit-conversationId group test now asserts the id is preserved instead of overwritten.

Checklist

🤖 Generated with Claude Code

https://claude.ai/code/session_015P6RBuagWTMzBmJLCiYKW9

@vercel

vercel Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

@Lukavyi is attempting to deploy a commit to the Vercel Team on Vercel.

A member of the Team first needs to authorize it.

@Lukavyi
Lukavyi force-pushed the telegram-group-session-mode branch from f7be94d to a735858 Compare September 4, 2026 07:15
@Lukavyi Lukavyi changed the title feat(eve): add groupSessionMode to telegramChannel feat(eve)!: key Telegram group sessions chat-wide Sep 4, 2026
@Lukavyi
Lukavyi force-pushed the telegram-group-session-mode branch 2 times, most recently from acb66a2 to 711f76b Compare September 4, 2026 07:24
@Lukavyi
Lukavyi force-pushed the telegram-group-session-mode branch from 711f76b to b55920e Compare September 4, 2026 07:48
@Lukavyi Lukavyi changed the title feat(eve)!: key Telegram group sessions chat-wide [1/2] feat(eve)!: key Telegram group sessions chat-wide Sep 4, 2026
@Lukavyi

Lukavyi commented Sep 4, 2026

Copy link
Copy Markdown
Author

Pushed 304b1ea: a reply to a bot message now carries its text as message on the same delivery as the synthetic telegram_reply:<id> response. The adapter still resolves a pending freeform answer first; when no prompt is registered, the text runs as a normal turn instead of the session re-parking without a turn. Also corrected the TelegramReceiveTarget docstring (conversationId pins an id that inbound never routes to; initialMessage stays chat-wide in groups). Telegram unit suite: 62 passed; tsc, guard:invariants, docs:check clean.

@Lukavyi
Lukavyi force-pushed the telegram-group-session-mode branch from 304b1ea to fc95f4a Compare September 4, 2026 08:09
Lukavyi and others added 2 commits September 4, 2026 10:10
Telegram group and supergroup inbound messages keyed the session to the
bot message being replied to, or to the mention itself. Every fresh
mention therefore started a new session with one message of context,
while private chats already keep one continuous session per chat.

Groups now key to the chat plus forum topic, the same way private chats
do. Mentions, replies to bot messages, callback queries, and outbound
sends all resume that session. The message_thread_id that non-forum
supergroups stamp on replies is a reply chain, not a topic, and does not
split the session. Proactive targets can still pin a thread through
conversationId.

Related to vercel#874

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015P6RBuagWTMzBmJLCiYKW9
Signed-off-by: Taras Lukavyi <lukavyi@me.com>
…orm prompt is pending

A reply to a bot message went through respond() with only the synthetic
telegram_reply:<id> response. When that id was not a registered freeform
prompt, the adapter resolved nothing, returned undefined, and the session
re-parked without a turn, so the reply was dropped. The initial channel
sent the text as message on the same delivery; vercel#1597 replaced that with
respond() and lost it. With chat-wide group sessions a reply to the bot
is the natural follow-up, so carry the text as message again: the adapter
still resolves a pending freeform answer first and otherwise runs the
text as a normal turn. Pass state and title so a reply can start a session
with a usable chat id when none owns the address.

Also correct the TelegramReceiveTarget docstring: conversationId pins a
caller-selected id that inbound messages never route to, and
initialMessage no longer starts a separate thread in groups.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015P6RBuagWTMzBmJLCiYKW9
Signed-off-by: Taras Lukavyi <lukavyi@me.com>
@Lukavyi
Lukavyi force-pushed the telegram-group-session-mode branch from fc95f4a to 9114ab6 Compare September 4, 2026 08:10
@Lukavyi
Lukavyi marked this pull request as ready for review September 4, 2026 08:17
@Lukavyi

Lukavyi commented Sep 4, 2026

Copy link
Copy Markdown
Author

Ready for review. @benpankow (recent Telegram HITL work) @ruiconti (merged #897, which this relies on): could one of you take a look? #3023 stacks on top with the observe half.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant