Telegram: route a reply to the session that sent the replied-to message - #415
Telegram: route a reply to the session that sent the replied-to message#415serxa wants to merge 6 commits into
Conversation
Replying (Telegram reply) to a message the bot sent now routes the reply to the session that produced that message — and makes it the active session — instead of the chat's active session. This makes it easy to answer messages from cron/background sessions and to talk to several sessions without an explicit /session switch. The channel records message_id -> (chat_id, session_id) for every outbound message (interactive send() and notification delivery) in a bounded LRU, and resolves it on inbound replies. It falls back to the active session when the mapping is unknown or the target session is gone/archived, and requires the chat id to match so a reply can never cross into a session bound to a different chat. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…uting A reply we can't tie to a live session — the replied-to message was never recorded / was evicted from the LRU, or its session is gone/archived — is now rejected with a static, no-LLM message asking the user to pick a session with /sessions and resend, rather than silently delivered to the chat's active session (which would be a mis-route to the wrong session). _resolve_reply() is now three-way (active / route / reject); the inbound handlers send the nudge and drop a rejected reply without starting a turn. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Record each inbound message against the session it is delivered to (the resolved reply target, else the active session), not just outbound bot messages. A user can then reply to their OWN earlier message to route the reply to that message's session — the same first-class targeting as replying to a bot message, and a clean way to re-target without /session. Adds _delivery_session(); both inbound handlers record the incoming message id -> target session before dispatch, and dispatch with that session explicitly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…/telegram-reply-routing
| """ | ||
| if not session_id: | ||
| return | ||
| self._reply_routes[message_id] = (chat_id, session_id) |
There was a problem hiding this comment.
I think the key here needs to be (chat_id, message_id), as message_ids are not unique by themselves.
Is this actually desirable? I think it may be quite surprising/unexpected behavior for users. Open to being convinced otherwise though. |
|
I was initially thinking that sticky is bad as well, but TBH, if it's not sticky, then you have to keep two things in your head - where you are answering and where the current active session is. This is tricky if you have many sessions. Imagine you started something and then replied to 5 different sessions. At this point, you probably do not remember where you started. Also, when you later look at chat history, it is completely unclear where the message goes. On the other hand, sticky resolves everything. This is the natural way chat works. Wherever you are, just continue the conversation in a way consistent with your previous message. It's 100% better IMO and keeps chat history clear and understandable. |
|
I personally would like to have this feature. But I talked with Nerve contributor @constkolesnyak before who originally introduced most of the Telegram integration and he said that the current behavior is intended. Let's put it under a feature flag in settings? |
Could you please elaborate on it a bit? If I understood correctly, currently replies allow to enrich somehow what is routed to the current session. I didn't use it, but anyway, this is unchanged, I believe. You can reply to a message generated on the same session - this is exactly the previous behavior. Slop mode: What you lose by enabling unconditionally: Quoting an arbitrary / cross-session message into your current session. A reply now either switches you to that message's session, or — if it's unmapped — rejects. So you can't reply to something from another session (or an old one) just to pull it in here. Workaround = copy-paste or a plain message. The "a reply always delivers" guarantee. An unmapped reply now rejects instead of landing in active. Sharpest edge: the map is in-memory, so right after any restart, replying to any pre-restart message (including the bot's last one) rejects until you exchange a few fresh messages — same for your own old messages or bot messages past the 2000 LRU cap. |
I'm okay with this if that's what we want. |
Telegram message ids are unique only within a chat, so keying the reply-route map on message_id alone let one chat's message clobber another chat's identically-numbered one — the clobbered chat's later reply would then miss and be wrongly rejected. Key on (chat_id, message_id) instead. Addresses review feedback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Put the new reply→origin-session routing behind an opt-in setting telegram.reply_routes_to_origin_session (default false), so the existing context-only reply behavior is unchanged unless enabled — addresses review feedback that the current behavior is intended. When off, no record / resolve / reject runs and a reply lands in the active session like a plain message. The flag is read live per message (no restart to toggle). Adds the setting to TelegramConfig + docs (settings + hot-reload tables); gates the recorder and both inbound handlers; adds on/off gate tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
What
Adds opt-in reply routing for Telegram, behind a new setting
telegram.reply_routes_to_origin_session(defaultfalse)./sessionswitch.How
When the flag is off, none of the machinery below runs and inbound replies are delivered to the active session (
session_idstays unset). When on:(chat_id, message_id)records the session for every message the bot sends (TelegramChannel.send+NotificationService._deliver_telegram, the cron case) and for every inbound message's landing session (_delivery_session). The key is(chat_id, message_id)because Telegram message ids are unique only within a chat._resolve_reply()classifies a message three ways — not a reply → active session; reply to a mapped, live session → route there + make active; reply that can't be resolved → reject.Safety (when on): an unresolvable reply is rejected, never mis-routed
A reply we can't tie to a live session — the target isn't in the map (sent before the daemon started, since the map is in-memory; or evicted from the LRU), its session is archived/gone, or the chat id doesn't match — is not delivered to the active session (that would be a silent mis-route). Instead the bot sends a static, no-LLM nudge ("couldn't find the session… use
/sessionsand resend") and drops the message without starting a turn.Behavior choice (when on)
Routing is sticky — a reply also makes the target session active (like
/session <id>), so a following plain message continues there.Config
telegram.reply_routes_to_origin_sessionfalseTesting
tests/test_telegram_reply_routing.py(20 tests): record/lookup,(chat_id, message_id)cross-chat isolation, LRU eviction/recency, three-way_resolve_reply,_delivery_session, reply-to-own-message routing,send()recording, and the flag gate (enabled reflects config; recording skipped when off).test_config_env,test_lockdown) reproduce on cleanmainin this environment and are unrelated to this change.