Skip to content

fix(telegram): stop typing refresh before final delivery - #64719

Open
seewilds wants to merge 2 commits into
NousResearch:mainfrom
seewilds:fix/telegram-typing-final-delivery-race
Open

fix(telegram): stop typing refresh before final delivery#64719
seewilds wants to merge 2 commits into
NousResearch:mainfrom
seewilds:fix/telegram-typing-final-delivery-race

Conversation

@seewilds

Copy link
Copy Markdown

Summary

  • stop the shared _keep_typing refresh task before the first final user-visible delivery
  • keep typing active through agent processing and optional auto-TTS generation
  • add a behavioral concurrency regression test that rejects typing calls after final delivery begins

Why

Commit 565b7c8 fixed #48678 by preventing TelegramAdapter.send() from directly calling send_typing() when metadata["notify"] marks a final response. That guard is necessary but insufficient: BasePlatformAdapter._process_message_background() independently starts a generic _keep_typing task with the unmarked thread metadata.

That task runs concurrently with final delivery. Telegram clears the current typing action when sendMessage arrives, but the refresh loop can race the delivery and issue another sendChatAction afterward, re-arming the indicator after the final reply.

The refresh task is now stopped after any auto-TTS generation but before play_tts, text, image, media, or file-only delivery. Intermediate and progress messages retain typing because the loop remains active until this final-delivery boundary. The existing outer finally cleanup remains in place as an idempotent safety net.

The new test starts a fast refresh loop, waits until typing is active, makes final delivery deliberately slow, and asserts that no typing calls occur once delivery begins. It fails on current main and passes with this fix.

Tests

  • scripts/run_tests.sh tests/gateway/test_typing_indicator_toggle.py tests/gateway/test_telegram_format.py -q — 112 passed
  • scripts/run_tests.sh tests/gateway/test_keep_typing_timeout.py tests/gateway/test_tts_media_routing.py tests/gateway/test_media_metadata_contract.py tests/gateway/test_tool_response_drop_recovery.py -q — 38 passed
  • uv run ruff check gateway/platforms/base.py tests/gateway/test_typing_indicator_toggle.py — passed

Production verification

The fix was verified against a live Telegram gateway after restart; the phantom typing indicator disappeared after final replies.

@alt-glitch alt-glitch added type/bug Something isn't working comp/gateway Gateway runner, session dispatch, delivery platform/telegram Telegram bot adapter P2 Medium — degraded but workaround exists sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Jul 15, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related: this enters the Telegram typing-indicator-race cluster. The most comprehensive open approach is #29172 (per-chat _typing_stop_events + stream-consumer coverage); this PR instead stops the shared _keep_typing task in BasePlatformAdapter before final delivery. Flagging for a maintainer to pick one approach across the cluster (also #21688, and closed #25210/#29919/#30732).

@tonydwb tonydwb 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.

Code Review Summary

Verdict: Approved

Overview

Fixes a Telegram UX bug: the typing indicator was being refreshed (reset) immediately before the final message delivery, causing a race where the indicator disappears before the message lands.

Assessment

  • Correctness: Stops the pre-delivery refresh — prevents the race condition cleanly.
  • Security: No security-sensitive changes.
  • Debug artifacts: None found.

Summary

Small, targeted fix. LGTM.


Reviewed by Hermes Agent

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for isolating the final-delivery typing race and adding a deterministic ordering regression. The race is present on current main: gateway/platforms/base.py:4908 starts _keep_typing, while the existing stop remains in final cleanup at gateway/platforms/base.py:5303, after delivery.

Problems

  • gateway/platforms/base.py:5036 adds a pre-delivery await of _stop_typing_task(), but current _stop_typing_refresh() catches CancelledError at gateway/platforms/base.py:3945 without distinguishing caller cancellation from the expected child-task cancellation. This can let a cancelled/stale processor continue into final delivery.
  • tests/gateway/test_typing_indicator_toggle.py:104 verifies ordering only; it does not cover cancellation while the newly pre-delivery typing shutdown is in progress.

Suggested changes

  • Preserve and re-raise caller cancellation after typing cleanup, while swallowing only the cancelled refresh task. Related PR #29172 contains a focused implementation and regression coverage for this distinction.
  • Add a cancellation regression proving no final text or attachment send starts after cancellation at this boundary.

Automated hermes-sweeper review.

Comment thread gateway/platforms/base.py
# Stop the refresh loop before the first final delivery. Some
# platforms clear typing when a message arrives, so a concurrent
# refresh can otherwise race the send and re-arm the indicator.
await _stop_typing_task()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This newly pre-delivery await makes caller cancellation safety load-bearing, but _stop_typing_refresh() currently catches CancelledError from the shielded wait without distinguishing cancellation of this processing task from expected child-task cancellation (gateway/platforms/base.py:3941-3948 on main). Preserve and re-raise caller cancellation here so a stale run cannot continue into final delivery.

@seewilds seewilds Jul 16, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks — addressed in commit 2b6593d. After the typing stop returns, the processor now checks its own cancellation state and re-raises CancelledError before TTS, text, or attachments can be delivered. Keeping the guard at the delivery boundary preserves the shared helper’s existing child-cancellation cleanup behavior; the new regression uses a text + PDF response and asserts neither send starts after cancellation.

@teknium1 teknium1 added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit labels Jul 16, 2026
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:blast-broad Sweeper blast radius: broad — a core path most sessions hit sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages 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.

4 participants