Skip to content

fix(telegram): use UTF-16 code units for message length splitting - #8698

Closed
teknium1 wants to merge 1 commit into
mainfrom
ironclaw-port/telegram-utf16-splitting
Closed

fix(telegram): use UTF-16 code units for message length splitting#8698
teknium1 wants to merge 1 commit into
mainfrom
ironclaw-port/telegram-utf16-splitting

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

Summary

Port from nearai/ironclaw#2304: Telegram's 4096 character limit is measured in UTF-16 code units, not Unicode codepoints. Characters outside the Basic Multilingual Plane (emoji like 😀, CJK Extension B, musical symbols) are surrogate pairs — 1 Python char but 2 UTF-16 code units.

The Bug

truncate_message() used Python's len() which counts codepoints. A message with 2049+ emoji (2049 codepoints = 4098 UTF-16 units) would pass the len(msg) <= 4096 check but exceed Telegram's actual limit, causing API errors.

The Fix

  • utf16_len(s) — counts UTF-16 code units via len(s.encode('utf-16-le')) // 2
  • _prefix_within_utf16_limit(s, limit) — binary-search truncation respecting surrogate pair boundaries
  • _custom_unit_to_cp(s, budget, len_fn) — maps a custom-unit budget to the largest safe codepoint slice position
  • truncate_message(..., len_fn=utf16_len) — optional length function parameter; Telegram passes utf16_len, all other platforms use default len() (zero behavior change)
  • Telegram error handler — uses _prefix_within_utf16_limit instead of codepoint slicing for the too-long fallback

Files Changed

File Change
gateway/platforms/base.py Add utf16_len, _prefix_within_utf16_limit, _custom_unit_to_cp; update truncate_message with len_fn param
gateway/platforms/telegram.py Pass len_fn=utf16_len to truncate_message; use _prefix_within_utf16_limit in error handler
tools/send_message_tool.py Use utf16_len when splitting for Telegram in send_message tool
tests/gateway/test_platform_base.py 22 new tests: TestUtf16Len, TestPrefixWithinUtf16Limit, TestTruncateMessageUtf16
tests/gateway/test_*_reply_mode.py Update mock lambdas to accept **kw for new len_fn parameter

Test Plan

  • 76 tests in test_platform_base.py pass (22 new)
  • 224 tests across telegram/discord format, reply mode, and stream consumer pass
  • Pre-existing failures confirmed unrelated (approval buttons, photo interrupts — fail on main too)

Practical Severity

Low-medium for typical agent output (mostly ASCII/Latin). Would trigger for users who receive messages with many emoji or CJK Extension B characters that push total UTF-16 length past 4096 while Python len() shows ≤ 4096.


Discovered via IronClaw PR Scout cron job — scanning nearai/ironclaw for features to port.

Port from nearai/ironclaw#2304: Telegram's 4096 character limit is
measured in UTF-16 code units, not Unicode codepoints. Characters
outside the Basic Multilingual Plane (emoji like 😀, CJK Extension B,
musical symbols) are surrogate pairs: 1 Python char but 2 UTF-16 units.

Previously, truncate_message() used Python's len() which counts
codepoints. This could produce chunks exceeding Telegram's actual limit
when messages contain many astral-plane characters.

Changes:
- Add utf16_len() helper and _prefix_within_utf16_limit() for
  UTF-16-aware string measurement and truncation
- Add _custom_unit_to_cp() binary-search helper that maps a custom-unit
  budget to the largest safe codepoint slice position
- Update truncate_message() to accept optional len_fn parameter
- Telegram adapter now passes len_fn=utf16_len when splitting messages
- Fix fallback truncation in Telegram error handler to use
  _prefix_within_utf16_limit instead of codepoint slicing
- Update send_message_tool.py to use utf16_len for Telegram platform
- Add comprehensive tests: utf16_len, _prefix_within_utf16_limit,
  truncate_message with len_fn (emoji splitting, content preservation,
  code block handling)
- Update mock lambdas in reply_mode tests to accept **kw for len_fn
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.

1 participant