Repository navigation
fix(telegram): streaming previews stop earning flood penalties — shared send+edit slot, retry_after honoured, word-boundary continuation (#116312, salvage #116385) - #116987
Conversation
|
Superseding #105340 with this is the right call, and the interval change is the The One narrow note, since it touches the same field from the other side. Thank you for the credit in the commit body. |
૮ >ﻌ< ა ci reviewran on 3488e53 — fix: slot-busy skipped Telegram edits no longer count as sho debug infoCI timingsCI timings · View report · View jobWall time 22m36s vs 45m35s (-50.4%). 8 job(s) slower, 2 faster, 2 unchanged.
|
When editing content grows past a streaming edit tick, the fallback prefix can end mid-word. Continuing from that cut drops the broken word's tail and reads as stutter. Back the cut up to the last space or newline so the tail of the split word is re-sent; a boundary-less prefix (one long token) keeps the original cut instead of re-sending the whole reply.
Telegram counts an editMessageText against the same per-chat allowance as a sendMessage, but streaming previews paced only edits (DEFAULT_STREAMING_EDIT _INTERVAL = 0.8s = 1.25 msg/s into one chat before any reply was sent) — 83% of measured flood penalties. One shared slot per chat: a send WAITS for its slot (skipping would drop a message), an interim edit is SKIPPED (the next tick shows the same text anyway), and the final edit is never gated (the answer is never withheld). A per-adapter tuning knob keeps the slot available to tests that model instantaneous bursts.
…er fix Keep interim-skip/final-never-gated + send-waits-for-slot, and mid-word/one-long-token for the continuation cut; the dropped cases restate the same two invariants.
…nstead of re-striking inside the penalty `_on_edit_failure` treated a flood-refused edit as a generic failure: a strike plus `min(interval * 2, 10)`. Starting from the 0.8s default that is 1.6s then 3.2s, so all three strikes (and three more refused requests, each extending the ban) were spent in about five seconds of a penalty Telegram had already told us is 9s or longer, and edits were then abandoned for the rest of the turn. - The interim interval now becomes `max(doubling, retry_after)` (capped at 30s; interim edits are skipped, not slept, so a long wait only costs a stale preview). - `_should_edit`'s `buffer_threshold` clause no longer overrides an active flood backoff: once the reply passed 24 codepoints every 50ms tick re-edited regardless of the interval, which made both the legacy doubling and any server wait dead letters. Slim redo of the retry_after half of #105340 (analysis by @AlexxRussell on #116312); the pause/join-budget machinery there is not needed once the interval itself is honoured.
…review follow-up)
An interim edit skipped because the chat's shared send+edit slot was busy
returned a plain SendResult(success=True), so the stream consumer recorded
the never-shown text as _last_sent_text and reset _flood_strikes. A later
turn-final flood then saw _visible_prefix() == final text and either marked
the turn delivered or entered fallback with an empty continuation — the user
never saw the tail. The adapter now flags the skip in
raw_response={"skipped": True} and _edit_existing leaves the visible prefix
and flood state untouched, so the next tick retries and a flood fallback
re-sends exactly the unseen tail.
a36ba68 to
3488e53
Compare
Telegram streaming no longer earns flood penalties from its own preview edits: sends and edits share one per-chat 1/s slot, a refused edit waits out the
retry_afterTelegram hands back, and the fallback continuation starts at a word boundary.plugins/platforms/telegram/adapter.py::_chat_outbound_slot_remaining/_hold_chat_outbound_slot, salvaged from fix(telegram): shared per-chat send+edit pacing budget and word-boundary continuation cut #116385): a send waits for its slot (never dropped), an interim preview edit is skipped while the slot is busy (the next tick shows the same text), the final edit is never gated. Measured by the reporter in production: 23 → 0 flood events/day at 2× the call density.gateway/stream_consumer_fallback.py::_continuation_text, from fix(telegram): shared per-chat send+edit pacing budget and word-boundary continuation cut #116385): the continuation re-sends the broken word's tail instead of starting mid-word.retry_afterhonoured on the edit path (gateway/stream_consumer_transport.py::_on_edit_failure, slim redo of the retry_after half of fix(gateway): honour Telegram flood waits on streaming edits #105340 per @AlexxRussell's analysis on the thread): interval becomesmax(doubling, retry_after)(30s cap) instead of 1.6s → 3.2s burning all three strikes inside a 9s+ penalty._should_editno longer letsbuffer_thresholdoverride an active flood backoff (gateway/stream_consumer.py): on main, once the reply passed 24 codepoints every 50 ms tick re-edited regardless ofedit_interval(probe:edit_interval=1.0→ 9 interim edits in 1.3 s), which made both the legacy doubling and any server wait dead letters.Root cause: Telegram counts
editMessageTextagainst the same per-chat allowance assendMessage, but the adapter paced only sends and the consumer's own interval was bypassed by the buffer-threshold clause and ignored the server'sretry_after.Live A/B (real
TelegramAdapter+ real event loop, fakeBotobject — no Bot API token on this host;scripts/probe_tg_pacing.py: 6 interim edits 0.1 s apart, then a send, then the final edit, one chat):Stream-consumer A/B (
tests/gateway/test_stream_edit_flood_retry_after.py, realGatewayStreamConsumer.run()against an adapter that answers every edit withflood_control:9,retry_after=9.0): before → 3 interim edits fired inside the penalty; after → 1, interval ≥ 9 s. Control (flood withoutretry_after) keeps the legacy doubling ladder.Tests:
scripts/run_tests.shon the 3 new files +test_telegram_typing_retrigger.py+ 16 mirroring stream-consumer / Telegram delivery files → 261 passed, 0 failed.Fixes #116312
Salvages #116385 (@HaisamAbbas — both commits cherry-picked, tests trimmed to two invariants per fix in a follow-up commit)
Supersedes #105340 (@AlexxRussell — retry_after honoured with a 5-line interval change instead of the pause/join-budget machinery; credited in the commit body)
Supersedes #116339 (paces inside the consumer only; the adapter-side slot is the design the reporter validated)
Dropped hunks
test_telegram_shared_outbound_budget.pyand 5 of 7 intest_stream_consumer_continuation_cut.py(restated the kept invariants).flood_pause_until/_wait_for_flood_pause/run_turn.py::_await_stream_taskjoin-budget extension and the turn-final single retry — not needed once the interim interval honoursretry_after; a turn-final edit refused mid-penalty still falls back to the existing send path and delivery ledger (fix(telegram): split replies arrive once and complete under flood control — resume from the refused chunk, per-chat send order and cooldown (#114396, salvage #52095, #114512) #114877).Infographic
Review follow-up