Repository navigation
feat(telegram): await polled handlers before advancing offset; combine incoming albums (#227) - #279
Conversation
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 9 minutes. View limit detailsLimit details: You’ve used the included review currently available. Review configuration: ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (5)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
# Conflicts: # docs/UPSTREAM_SYNC.md
…reply_to, document stop divergence, cover album/polling contract (#227)
# Conflicts: # docs/UPSTREAM_SYNC.md
# Conflicts: # CHANGELOG.md
|
Merge gate: CI green (test 3.12, test 3.13, Lint & Type Check, CodeQL / Analyze python+actions) on 1687ec0 (merge of origin/main into fbd5f4b; PR's src/tests/scripts diff vs main is byte-identical to the reviewed one, main's new changes touch only linear/whatsapp/ai; only CHANGELOG conflicted, both sides kept; fidelity target unchanged 151 -> 151). Local Codex review (gpt-6-astra, xhigh, --base origin/main) on fbd5f4b: "No actionable regressions found relative to the supplied merge base. All 508 Telegram tests passed, and changed-file lint and formatting checks passed." 6 astra rounds; CodeRabbit rate-limited (no review), no other bot findings. Merging with --admin (Protect Main requires a code-owner approval). |
Summary
Telegram long polling used to advance
offsetbefore any handler ran, so a handler failure or a crash lost the update. Now it works like upstream 4.41:offsetmoves past it. Failed updates are saved in atelegram:polling:{sha256(bot_user_id)}checkpoint ({offset, pending: [{update, receivedAt, attempts, retryAt}]}) and retried with backoff. Polled updates are dispatched withWebhookOptions(deduplicate=False); the checkpoint deduplicates them, not core dedupe.media_group_idare buffered in state (with a lock) and dispatched as oneMessage1 s after the newest part. This works on both the webhook and polling paths. On polling, album parts wait in the checkpoint for the settle window and the album succeeds or fails as a unit.process_updatereturns the dispatched tasks: message, album, slash command, action and reactions.Split: outbound multi-file / multi-attachment
sendMediaGroup(8d7ccdb1, #605) is moved to #278. With it, this PR would have been about 1.9k LOC; the issue's Metadata says to split the outbound part off if it runs over. So this PR isPart of #227, notCloses.Upstream commits mapped
629e6555fix(telegram): combine incoming media groups (#760), chat@4.37.0handle_incoming_message_update→_start_incoming_media_group/_process_incoming_media_group; slash gate skips album parts91683e52fix(telegram): wait for polling handlers before advancing offset (#942), chat@4.41.0, Telegram halfprocess_updatereturns tasks;polling_loop,_process_polling_updates,_polling_group,_fetch_polling_updates(thecollectingabort timer),_await_update_tasks26a06ca5lazy identity retry_ensure_bot_identity()runs on every loop iteration8d7ccdb1(#605)The core half of #942 (
WebhookOptions.deduplicate,process_*returning tasks that raise) was already on main from #191. I checked this before starting.Tests ported (
tests/test_telegram_webhook.py)From
adapter-telegram/src/index.test.ts:From
integration-tests/src/polling.test.ts, run against a realChat+MemoryStateAdapterand a fake Bot API that redelivers unacknowledged updates:dedupe:key already set)retry_aftergetMe(fromindex.test.ts; asserts the checkpoint key, sincementionOnReplyis [4.41/TG4] Telegram replies: replied-to context, reply-to-bot as mention, native replies, portable file data #228)Python-specific:
stop_pollingduring an album settle returns promptly with a consistent checkpoint, and a restart delivers the album oncestop_pollingwaits for an in-flight handlerChat.shutdownkeeps its update pendingwait_untilsettles, while the returned task still raisesdisconnect()cancelling a settling webhook album leaves thewait_untiltask settled (resolves toNone), not cancelledis_mentionif any part mentions the bot, links concatenated,reply_tofrom the first part that has oneThe tests use an injectable fake clock (
_now_ms/_sleep) and never sleep on the wall clock. I also mutation-checked them: each of 20 mutations (the 12 below, plus the_settledcancel handling,reply_to, the per-iteration_ensure_bot_identity(),timeout=0when a retry is due, the newest-10 cap,is_mentionfrom any part, links, and the redelivery filter) (dropping the handler await,deduplicate=False, the album slash gate, the cancelled-handler failure, thecompletedset, unconditional cancel instop_polling,retry_after, task collection for message/action/reaction, the allowlist group check, anddrained) makes at least one test fail.Fidelity
TS_ROOT=…/vercel-chat-4.41.1 verify_test_fidelity.py --report-target:Delta vs committed report (HEAD): missing 160 -> 160 (+0).adapter-telegramis not fidelity-mapped (#78), soscripts/fidelity_target.jsonis unchanged. Strict at the 4.31.0 pin passes ("All TS tests have Python equivalents").Divergences
All three are recorded in the new
docs/UPSTREAM_SYNC.mdsection "Telegram polling acknowledgement and incoming albums".stop_pollingcancels the loop only while it is parked ingetUpdatesor a polling sleep, which are the awaits upstream's signal reaches. While handlers run or the checkpoint is written, it waits, as upstreamstopPollingdoes. It waits withasyncio.wait, so cancelling the caller still propagates.Chat.shutdowncancels in-flight handler tasks (upstream waits for them). Without this, a cancelled handler would be acknowledged unhandled; now the update stays in the checkpoint.stop_pollinglands while the loop awaits_ensure_bot_identity()or the checkpointstate.get, the loop returns before dispatching the ready retry batch. UpstreampollingLoop(adapter-telegram/src/index.ts:4057-4088) has nopollingActivecheck there and still dispatches it; in Python that batch could start handlers afterChat.shutdown's cancellation sweep. The batch stays in the checkpoint for the next start.reply_toon the combined album message is carried as upstream (index.ts:1295, first part that has one) now thatMessage.reply_toexists (#192); Telegram parsing populates it only from #228, so it isNoneat runtime today.Clock:
receivedAt/retryAtare epoch ms (as upstreamDate.now()). They are persisted, so a monotonic clock would not survive a restart.Astra review (gpt-6-astra)
Round 5: PASS ("No actionable regressions found").
Fixed:
fetch.result()before the cancelled fetch had finished, which raisedInvalidStateError. Every album settle therefore went through the failure backoff. It now awaits the fetch first. Regression: the coalesce test asserts no "Telegram polling request failed" warning.Chat.shutdown's handler cancellation.disconnect()now cancels_media_group_tasksbefore stopping the poller, and the album stays in the checkpoint. Regression:test_chat_shutdown_during_album_settle_does_not_wait_for_the_album.stop_pollingduring the initial checkpoint read could still dispatch saved retries. The loop now rechecks_polling_activefirst. Regression:test_stop_during_the_checkpoint_read_dispatches_no_saved_retry.Not changed, because the behavior is identical to upstream chat@4.41.1 (I added a code comment at each site):
adapter-telegram/src/index.ts:1216).queue/debounceconcurrency, a drained handler's failure is attributed to the update that drains the queue (chat.ts:2646-2729).index.ts:1248-1249).stop_pollingdeadlocks; upstreamstopPollingawaitspollingTask(index.ts:846-847).Chat.shutdown's Python-only cancellation sweep is admitted work thatstop_pollingwaits for, as upstream does.None of these was re-raised after the comment.
Consumer impact
Telegram only; none for Slack or Teams.
/cmdis no longer routed as a slash command.process_updateand thehandle_*helpers now return tasks instead ofNone/bool.Validation
ruff check/format, audit (0 hard failures),
--check-docs,--strict(pin 4.31.0), pytest6807 passed, 24 skipped(after merging origin/main at98e0852), pyrefly 0 errors.Part of #227
Part of #184
Merge gate
Final HEAD:
fbd5f4b(merged origin/main at98e0852; resolved thedocs/UPSTREAM_SYNC.mdconflicts twice (#192/#197 and #229) by keeping both sections;fidelity_target.jsonregenerated with no change).Independent review findings (7):
wait_untilalbum wrapper (_settled) ended cancelled whendisconnect()cancelled a settling album. It now mirrorsChat._tracked._await_quietly: an album cancellation counts as settled, and only a cancellation of the wrapper itself propagates. Regression:test_waituntil_settles_when_disconnect_cancels_a_settling_album(fails without the fix withCancelledError). This was reported twice, as a parity finding and a robustness finding.reply_tofrom the first part that has one, as upstreamindex.ts:1295does, now thatMessage.reply_toexists ([4.41/C2a] Core mentions & message model: tri-state is_mention, mention regex, Author.email/is_system, Message.reply_to #192). The docs no longer say the field is missing. Regression: covered in the album contract test.docs/UPSTREAM_SYNC.md, the CHANGELOG and the code comment (divergence 3 above). I kept the behavior._ensure_bot_identity()was untested. Addedtest_recovers_bot_identity_while_polling_after_a_failed_startup_get_me.is_mentionfrom any part, links, newest-10 cap, the redelivery filter,timeout=0when due). Added the album contract test, atimeout == 0assertion, and ports of "processes beyond a full page despite a permanently failing %s" and "combines an album spanning a full polling response". All 8 targeted mutations now fail the Telegram suite.gpt-6-astra: 1 round on
fbd5f4b. Verdict PASS: "No actionable regressions found relative to the supplied merge base. All 508 Telegram tests passed, and changed-file lint and formatting checks passed."Bots: CodeRabbit skipped the draft and was then rate-limited ("Review limit reached"). There were no inline or review comments from CodeRabbit or gemini.
CI on
fbd5f4b: Tests 3.12/3.13 pass, Lint & Type Check pass, CodeQL / Analyze pass.Local: ruff check/format, audit (0 hard failures),
--check-docs,--strictat the 4.31.0 pin, pytest 6807 passed / 24 skipped, pyrefly 0 errors.