Skip to content

feat(telegram)!: post-and-edit streaming by default, native drafts opt-in, link-preserving truncation (#226) - #296

Merged
patrick-chinchill merged 6 commits into
mainfrom
sync/4.41-tg2
Oct 1, 2026
Merged

patrick-chinchill merged 6 commits into
mainfrom
sync/4.41-tg2

Conversation

@patrick-chinchill

@patrick-chinchill patrick-chinchill commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Telegram streaming and MarkdownV2 truncation, matching chat@4.41.1.

  • stream() uses native draft bubbles only when the new native_streaming=True is set and the chat is a DM. Every other stream, DMs included by default, now runs through the new adapter-owned _post_and_edit_stream, and stream() never returns None.
  • _post_and_edit_stream is a port of upstream postAndEditStream:
    • Edits are at least 1100 ms apart in private chats and 3100 ms in other chats. The new streaming_edit_interval_ms overrides the floor, and a larger update_interval_ms still wins.
    • Intermediate edits that fail are logged and skipped, and a 429 holds later edits until its retry_after has passed.
    • The final edit waits for pacing and any rate limit, retries once after a 429, and raises if the wait would exceed 5 s or the retry fails.
  • Placeholder: UNSET posts "...", None posts no placeholder, and a string is posted as given ("" is rejected by post_message). This uses the tri-state from [4.41/C5] Streaming & thread API: lightweight threads, placeholder sentinel, restore streaming settings, Plan.auto_complete_previous, AG-UI streams #199.
  • truncate_for_telegram returns text that fits the limit unchanged, in every parse mode. Over the limit, _trim_to_markdown_v2_safe_boundary makes a single _scan_delimiters pass (upstream scanDelimiters) that skips code, escapes and link URLs. It pairs __ separately, treats two trailing backticks as a cut fence, and does not count a ] that ends the slice as closing a link. Three helpers that are now dead were removed.

Upstream commits mapped

Upstream Version Status
3bbf3ff5 fix(telegram): make native draft streaming opt-in (#822) 4.38.0 ported
745fdf5a fix(adapters): harden Telegram streaming… (#826), Telegram half 4.38.0 ported (XChat half N/A)
43dba3de perf(telegram): skip unused plain draft rendering (#900) 4.40.0 code already equivalent; test ported
f893470e fix(telegram): preserve links during truncation (#915) 4.41.0 ported
937cac98 fix(telegram): … URLs with entity-marker characters (#610) 4.32.0 already ported; its helpers are replaced by #915's scanner and its cases are covered by the #915 tests

Tests ported

index.test.ts and markdown.test.ts are not fidelity-mapped (#78), so the ports are in tests/test_telegram_streaming.py and tests/test_telegram_api.py.

  • Streaming:
    • streams draft updates when native streaming is enabled
    • streams non-DM chats with post-and-edit even when nativeStreaming is on
    • streams private chats with post-and-edit by default
    • uses the private chat streaming edit floor
    • uses the group streaming edit floor
    • edits per chunk when streamingEditIntervalMs opts out of throttling
    • retries the final streamed edit once…
    • rejects when the final streamed edit retry fails
    • rejects when the final retry_after exceeds the cap
    • respects retry_after after an intermediate streamed edit
    • it.each renders plain text only when selected for rich / markdown / plain native drafts
  • Truncation (index.test.ts): preserves URL backticks and trailing text in a legacy MarkdownV2 post, in a legacy MarkdownV2 edit, and in a MarkdownV2 file caption.
  • markdown.test.ts, describe("truncateForTelegram"):
    • returns MarkdownV2 that fits the limit unchanged
    • trims back to before the [ inside a link URL
    • trims at the orphan *
    • trims at the __ opener
    • keeps a rendered link with odd underscores
    • retreats before a partial link at every destination cut
    • retreats before code (it.each)
  • markdown.test.ts, describe("trimToMarkdownV2SafeBoundary"): every case, including the "link URLs with raw entity-marker characters" block.
  • Python-only tests:
    • placeholder tri-state (custom, None, None with held-back text, None with no text, "")
    • a rate-limited closing edit retried after retry_after (None / non-numeric / numeric)
    • a failed retry of the closing edit raises
    • a closing-edit 429 over the cap raises
    • an intermediate 429 within the cap delays the final edit
    • an intermediate non-429 failure is logged and skipped
    • cancellation during the pacing wait propagates CancelledError
    • config clamping
    • a larger update_interval_ms beats the floor
  • Replaced:
    • test_strips_unpaired_marker_under_limit_streaming_leak
    • test_returns_null_for_nondm_streaming_so_chat_sdk_can_use_fallback_streaming
    • the find_unescaped_positions tests
    • under-limit link tests, which are now trivially true (their unique cases were retargeted at the trimmer)
    • The DM draft tests now set native_streaming=True.
  • Mutation check: I applied 20 mutations by hand (scanner branches, pacing, cap, retry, placeholder sentinel, config guard). The test suite caught every one.

Fidelity

Delta vs committed report (HEAD): missing 104 -> 104 (+0) (after merging main at cb90b85). The Telegram test files are outside MAPPING / TARGET_MAPPING (#78), so scripts/fidelity_target.json is unchanged. The strict check at the pin is 733/733.

Divergences

There are no new rows in the non-parity table.

  • Python-surface adaptations, documented in docs/UPSTREAM_SYNC.md under "Telegram streaming and MarkdownV2 truncation":
    • Pacing uses an overridable _monotonic_ms() (time.monotonic), not the epoch _now_ms() that is persisted for polling.
    • streaming_edit_interval_ms ignores bool, matching upstream's typeof check.
    • A non-finite or non-numeric retry_after counts as missing (1 s), because Python would multiply a string by 1000.
  • Test adaptation: the Python parser has no GFM autolink literals. "preserves an autolinked bare URL containing a backtick" therefore runs the trimmer on the string upstream renders.
  • Existing core divergence row narrowed: core clears the placeholder to " " when a fallback stream produces only whitespace. That no longer applies to Telegram, whose own loop now leaves "..." on a whitespace-only stream, as upstream's does.

Consumer impact (BREAKING for Telegram streaming)

  • DMs stream as a posted "..." message that is edited as text arrives, not as a draft bubble. Set TelegramAdapterConfig(native_streaming=True) to get drafts back.
  • Group, supergroup and channel streams now edit at most every 3.1 s, instead of the core fallback's 500 ms. Set the floor with streaming_edit_interval_ms (0 edits on every chunk).
  • When the final edit cannot be delivered, the stream raises AdapterRateLimitError or the edit's own error. It no longer returns a message that still shows truncated text.
  • MarkdownV2 under the 4096 / 1024 limit is sent unchanged, so the chat#446 under-limit trim is gone. All MarkdownV2 is rendered from an AST, and parse errors still fall back to plain text. The module-level helpers find_unescaped_positions, _find_unescaped_positions_outside_code and _find_unclosed_link_dest_open_bracket were removed; none was exported from the package.
  • An exception raised by the text stream propagates without a final edit, as upstream. In groups, the core fallback used to flush the partial text first; now a stream that fails before the first paced edit leaves the "..." placeholder (or the last paced edit) visible.
  • Slack and Teams streaming are unaffected.

Verify-first results

  • No send path depends on the under-limit trim. resolve_parse_mode sends plain strings and raw payloads with no parse mode. Every MarkdownV2 body is rendered by TelegramFormatConverter, and a can't parse entities rejection still retries as plain text through with_telegram_markdown_fallback.
  • The 937cac98 behaviour is covered by the new scanner. It skips the link URL, and the "preserves a link whose URL contains entity markers" and "keeps a rendered link whose URL has odd underscores" cases pass.

Validation

Full validation from the task passed:

  • ruff check and format are clean.
  • The audit reports 0 hard failures.
  • --check-docs passes, and --strict reports 733/733.
  • pytest: 7506 passed, 24 skipped (after merging main at cb90b85).
  • pyrefly check reports 0 errors.

Review (gpt-6-astra)

Round 3 PASS (on 0c8cb00): no actionable findings. Rounds 1 and 2 each raised two P2 findings, and in all four the flagged behavior is identical to upstream chat@4.41.1. I did not add divergent machinery; each site has a code comment noting upstream parity, and docs/UPSTREAM_SYNC.md records the behavior.

  • R1: a "message is not modified" 400 on the final edit propagates. Upstream flushFinalEdit (index.ts:2069-2102) retries only AdapterRateLimitError.
  • R1: an exception from the text stream skips the final flush. Upstream's for await loop (index.ts:2104-2138) has no try/finally.
  • R2: the first edit after a plain placeholder can fail on an old Bot API server. Upstream does the same (index.ts:2019-2049). The core fallback already took this path for groups before this PR.
  • R2: a non-429 edit failure does not advance pacing. Upstream applyEdit sets lastEditAt only on success (index.ts:2025-2036).

Merge gate

Independent review findings (4):

  • Fixed: docs/UPSTREAM_SYNC.md row "Fallback streaming stream-exception capture" still applied to Telegram. It is now titled "(non-Teams, non-Telegram adapters)" and says Telegram's post-and-edit loop propagates stream errors without a partial flush, as upstream postAndEditStream does. Two reviewers raised it.
  • Fixed: the CHANGELOG BREAKING entry has a new "stream errors" sub-bullet for the lost partial flush in Telegram groups.
  • Fixed: the pacing tests could not catch a dropped pacing timestamp, because _FakeClock started at 0, which equals the initial last_edit_at. The clock now starts at 10 000 ms, and the floor tests assert times relative to that start. A new test, test_no_placeholder_paces_edits_from_the_first_post, covers the no-placeholder path. A retry_after of exactly 5 s is now covered on both cap checks (a strict >, as upstream index.ts:2077 / :2094). I mutation-checked the four mutants the reviewer reported, and each one now fails the tests: removing either last_edit_at stamp (9 and 1 failures), and changing > to >= on either cap check (1 failure each).
  • Fixed: two Python-only trimmer tests duplicated upstream ports (CLAUDE.md principle 3) and were deleted. I confirmed that a mutant that lets an escaped ) close a link URL still fails the ported tests.

gpt-6-astra: after this round's fixes and the merge of main, the run on cb90b85 is PASS: "No actionable regressions were found". An earlier attempt on the same SHA was interrupted by model capacity and re-run. There were 3 rounds before this one; see above.

Bots: CodeRabbit skipped the PR while it was a draft. There are no inline or review comments to handle.

CI on cb90b85: Tests (3.12 and 3.13), CodeQL and Analyze are green. The PR's Lint & Type Check check is shown as skipped, because it ran on the push while the PR was still a draft; marking it ready did not start a new run. I ran the same workflow with workflow_dispatch on cb90b85 and it passed: https://github.com/Chinchill-AI/chat-sdk-python/actions/runs/36830341966.

Local full validation on cb90b85: ruff check and format are clean, the audit reports 0 hard failures, --check-docs passes, --strict reports 733/733, pytest has 7506 passed and 24 skipped, and pyrefly reports 0 errors.

Closes #226
Part of #184

…t-in, link-preserving truncation (#226)

Ports vercel/chat 3bbf3ff5 (#822) and the Telegram half of 745fdf5a (#826)
from chat@4.38.0, and f893470e (#915, chat@4.41.0); test-only port of
43dba3de (#900).

- stream(): native drafts only when native_streaming and the chat is a DM;
  everything else goes through the adapter-owned post-and-edit loop
  (1100 ms private / 3100 ms group edit floors, retry_after handling, final
  edit retried once and raised on failure). Never returns None.
- TelegramAdapterConfig gains native_streaming and streaming_edit_interval_ms.
- truncate_for_telegram returns under-limit text unchanged (drops the
  chat#446 under-limit trim); the over-limit trimmer is one
  _scan_delimiters pass that skips code, escapes and link URLs and pairs
  __ separately from _.

BREAKING CHANGE: Telegram DMs stream via post-and-edit by default; set
native_streaming=True for draft bubbles. Group edits are paced at 3.1 s.

Closes #226
@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You'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 39 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 4c46833f-bfba-465f-bab0-fb2e732decae

📥 Commits

Reviewing files that changed from the base of the PR and between 49556ca and d2ee325.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • docs/UPSTREAM_SYNC.md
  • src/chat_sdk/adapters/telegram/adapter.py
  • src/chat_sdk/adapters/telegram/types.py
  • src/chat_sdk/thread.py
  • tests/test_telegram_api.py
  • tests/test_telegram_streaming.py
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@patrick-chinchill
patrick-chinchill marked this pull request as ready for review October 1, 2026 07:22
@patrick-chinchill

Copy link
Copy Markdown
Collaborator Author

Merge gate: CI green on d2ee325 (Lint & Type Check, test (3.12), test (3.13), Analyze (python), Analyze (actions), CodeQL, CodeRabbit). Local Codex review (gpt-6-astra, xhigh, --base origin/main) on cb90b85: "No actionable regressions were found against the specified merge base. All 587 Telegram tests, 229 thread tests, and lint checks for changed Python files passed." That took 4 completed astra rounds. d2ee325 only merges origin/main (#293, Teams installation/join events, which doesn't touch the Telegram or thread modules). The only conflict was in CHANGELOG.md, resolved by keeping both bullets. The full local validation passed: 7560 passed, strict fidelity 730/730 real, pyrefly 0 errors, fidelity target delta +0. Bots: CodeRabbit rate-limited/draft-skipped, no human or bot reviews outstanding. Merging with --admin (Protect Main requires a code-owner approval).

@patrick-chinchill
patrick-chinchill merged commit 8d3c35c into main Oct 1, 2026
7 checks passed
@patrick-chinchill
patrick-chinchill deleted the sync/4.41-tg2 branch October 1, 2026 07:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[4.41/TG2] Telegram streaming: native draft streaming opt-in, streaming hardening, link-preserving truncation

1 participant