feat(telegram): interactive /sessions picker with IDOR-safe state - #89356
feat(telegram): interactive /sessions picker with IDOR-safe state#89356nothingness0db wants to merge 1 commit into
Conversation
63464cc to
12232b1
Compare
Reviewed the diff. This is the right architecture for an interactive picker over an authorization-sensitive action: the runner hands the adapter the FULL origin-scoped row list (picker paginates internally instead of the text 10-cap silently hiding sessions), the adapter stays transport-only with composite (chat_id, msg_id, thread_id) state keys so forum topics and double-/sessions cannot collide, stale keyboards are rejected by msg_id before anything runs, taps get a cheap adapter-level authorization gate mirroring the approval/choice pickers, and the runner callback re-runs
No blocking issues found. |
|
Indeed |
Address reviewer feedback from the AI code review: 1. Picker UX redesign — preview moved to message body (every Telegram client renders the same), buttons show just page-relative numbers. Single-line body format keeps title + preview visible without the "stacked clutter" the original two-line layout produced on mobile. 2. Memory leak fix — _sessions_picker_state entries now carry a created_at timestamp and are swept on every send_sessions_picker call (lazy TTL, 1h default). Without this, busy groups accumulate dead pickers holding closures over the runner callback. 3. State key cleanup — nav-row page-indicator uses the dedicated "sp:" prefix (was "sx:noop" co-habiting with the cancel "sx:" namespace), so future nav verbs don't collide with cancel handling. 4. MarkdownV2 escaping — title and preview are now escaped for the parse_mode on every body line (was missing for the new body format). Tests: 51 pass. Deployed + verified on Telegram.
13d6959 to
8747a3a
Compare

Summary
Replace the numbered text reply for /sessions\ on Telegram with an inline-keyboard picker. Each session is a button; tapping it resumes that session directly. The currently-active session is filtered out so the list only shows resumable targets. Pickers paginate when there are more than 8 sessions (\◀ Prev\ / \1/N\ / \Next ▶), and a \✗ Cancel\ button dismisses the picker. Buttons render the title on its own line and the first-message preview on a second line (truncated to 40 chars, 64-char Telegram cap).

Built on the same picker state pattern as the existing model picker / choice picker / approval picker — but with the security-aware fixes that the blocker sweep on #43695 and #49038 flagged:
IDOR guard at the runner boundary. The picker's \on_session_selected\ callback re-runs _resume_target_allowed\ with the captured \SessionSource\ before delegating to the shared _resume_session_by_id\ helper. A co-member in a shared group cannot tap a button to bind to another user's persisted session — same gate the text /resume \ path uses. The adapter also re-checks via _is_callback_user_authorized\ for a cheap fail-closed layer.
Collision-safe state key. State is keyed by (chat_id, msg_id, thread_id)\ instead of \chat_id\ alone. A second /sessions\ opened in the same chat (forum threads, /sessions called twice in a row) cannot overwrite the first picker's state, and a stale click on the old keyboard after a new /sessions\ has replaced it is rejected at the adapter before the runner is invoked.
Session-switch via the funnel. _resume_session_by_id\ uses \�sync_session_store.switch_session\ and calls _release_running_agent_state\ + _clear_conversation_scope\ + _evict_cached_agent\ — the same funnel that fixed the bug-class regressions Fix gateway /resume leaking cached agent state across session switches #10702, /new doesn't reset model config — old session keeps using stale model after config change #58403, feat(memory): notify memory providers on in-chat /resume and /branch session switches #6672. The text /resume\ path still uses the inline switch logic (kept distinct to preserve the Matrix --cross-room\ branch which needs source-object-aware title substitution).
Origin-scoped listing. The picker receives only the rows the runner already filtered through _resume_row_visible\ + _resume_target_allowed\ — same scope the text list uses, so the picker cannot bypass the IDOR guard that the listing already enforces. The picker branch lifts the legacy 10-cap (text fallback still caps at 10) so the picker can paginate through the full origin-scoped list (up to 50).
Authorization gate at the adapter. Mirrors the approval / choice picker pattern: a co-member tap is rejected at the Telegram adapter before the runner callback runs.
Tests
\
tests/gateway/test_telegram_sessions_picker.py — 16 tests (adapter state, IDOR, pagination, collision, current-session filter)
tests/gateway/test_sessions_command_picker_integration.py — 6 tests (runner ↔ adapter wiring, fallback, full-list cap)
\\
Regression-safe: the existing 27 tests in \ est_resume_command.py\ (incl. \TestHandleSessionsCommand\ and \TestSameOriginChatGroupScoping) all still pass.
\uv run --no-sync python -m pytest tests/gateway/test_telegram_sessions_picker.py tests/gateway/test_sessions_command_picker_integration.py tests/gateway/test_resume_command.py\ → 49 passed.
Why not just merge #43695 / #49038
Both upstream PRs were kept open by the hermes-sweeper with the same blockers:
Salvage credit: the pagination shape, cancel button, and adapter scaffold follow the pattern eltecnicowd opened in #49038. Rebased on top of the current \plugins/platforms/telegram/adapter.py\ (the path #43695 conflicted on).
🤖 Generated with opencode