Skip to content

fix(gateway): rebind Telegram DM topic on every session-switch surface - #106227

Open
jean-luc2305 wants to merge 1 commit into
NousResearch:mainfrom
jean-luc2305:fix/telegram-topic-binding-switch-surfaces
Open

jean-luc2305 wants to merge 1 commit into
NousResearch:mainfrom
jean-luc2305:fix/telegram-topic-binding-switch-surfaces

Conversation

@jean-luc2305

Copy link
Copy Markdown

Summary

Topic mode persists a (chat_id, thread_id) -> session_id row in telegram_dm_topic_bindings so reopening a Telegram DM topic resumes the right Hermes session, and so the retire/archive helpers resolve the correct topic to delete. /new already pairs its switch_session with _record_telegram_topic_binding (regression: test_new_inside_telegram_topic_rewrites_binding_to_new_session), but four other surfaces call SessionStore.switch_session WITHOUT the paired rebind, so the binding goes stale (keeps pointing at the old session) or is never written:

  1. /resume — _handle_resume_command (gateway/slash_commands_session.py)
  2. /branch — _handle_branch_command (gateway/slash_commands_session.py)
  3. CLI-handoff worker — _process_handoff (gateway/run_startup.py)
  4. async-delegation completion — _resolve_async_delegation_session (gateway/run_notifications.py)

A stale binding is a retire-safety hazard: the archiver resolves its delete target from the binding row, so a wrong row means deleting the wrong Telegram topic. In practice it also drove one thread 15 sessions deep on a single stale row via the handoff worker.

Fix

Add GatewayTopicThreadsMixin._rebind_telegram_topic_after_switch(source, session_entry) — guarded by _is_telegram_topic_lane (no-op outside a Telegram DM topic lane), run off-loop via asyncio.to_thread, best-effort like the /new path — and call it after switch_session in all four surfaces.

The async-delegation compression branch (advance_compression_session) already syncs the binding, so only its plain switch_session path is rebound. No schema change.

Tests

Adds tests/gateway/test_telegram_topic_binding_switch_surfaces.py, which drives each of the four surfaces against a real SessionStore + SessionDB (SQLite in tmp_path, topic mode enabled) and asserts the binding follows the switch.

  • Before the fix: all 4 fail (binding stays on the old session).
  • After the fix: all 4 pass; neutering the helper flips them back to RED (load-bearing verified).
  • Existing test_telegram_topic_mode.py + test_branch_routing_columns.py still green; broader tests/gateway -k "topic or branch or resume or handoff or delegation or notification or slash" sweep: 518 passed, 2 skipped.

Checklist

  • Conventional Commit message (fix(gateway): ...)
  • PR contains only changes related to this fix
  • Tests added and passing (RED→GREEN, load-bearing verified)
  • No schema change

Topic mode persists a (chat_id, thread_id) -> session_id row in
telegram_dm_topic_bindings so reopening a topic resumes the right Hermes
session and the retire/archive helpers resolve the correct topic to delete.
/new already pairs its switch with _record_telegram_topic_binding, but four
OTHER surfaces call SessionStore.switch_session WITHOUT the paired rebind, so
the binding goes stale (keeps pointing at the old session) or is never written:

  1. /resume  (_handle_resume_command)
  2. /branch  (_handle_branch_command)
  3. CLI-handoff worker (_process_handoff)
  4. async-delegation completion (_resolve_async_delegation_session)

A stale binding is a retire-safety hazard: the archiver resolves its delete
target from the binding row, so a wrong row means deleting the wrong Telegram
topic. It also drove one thread 15 sessions deep on a single stale row via the
handoff worker.

Fix: add GatewayTopicThreadsMixin._rebind_telegram_topic_after_switch (guarded
by _is_telegram_topic_lane, off-loop via asyncio.to_thread, best-effort like the
/new path) and call it after switch_session in all four surfaces. The
async-delegation compression branch (advance_compression_session) already syncs
the binding, so only its plain switch_session path is rebound. No schema change.

Adds tests/gateway/test_telegram_topic_binding_switch_surfaces.py driving each
surface against a real SessionStore + SessionDB with topic mode enabled and
asserting the binding follows the switch.

@andrexibiza andrexibiza left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The switch-surface coverage is pointed at the right class, but the new helper still leaves the retire-safety invariant fail-open. _rebind_telegram_topic_after_switch() catches every persistence failure, logs it, and returns after the route has already moved. The PR itself establishes that a stale (chat_id, thread_id) -> session_id row is mutation authority for archive/delete and can therefore target the wrong Telegram topic. Once switch_session succeeds, silently retaining the old durable binding is not a best-effort telemetry failure; it is a split-brain owner state.

Please make the paired transition settlement-safe: either the rebind must succeed before the switch surface publishes success, or a failed rebind must explicitly invalidate/fence the stale binding so it cannot be consumed as delete/resume authority. Add a failure-path regression that forces _record_telegram_topic_binding to fail after the routing move and proves the old binding cannot remain actionable. The four GREEN happy-path tests currently cannot detect this class.

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery platform/telegram Telegram bot adapter sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Sep 9, 2026
@alt-glitch

Copy link
Copy Markdown

This was generated by AI during triage.

Related: #58850 (issue: /fork in a Telegram DM topic routes back to the parent) and its open fix #58851, which rebinds the topic for /branch only. This PR covers that surface plus /resume, the CLI-handoff worker and async-delegation completion, so the two PRs overlap on the /branch path.

This branch has not been deployed

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

Labels

comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists platform/telegram Telegram bot adapter sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants