Skip to content

fix(telegram): repair model-collapsed/pre-escaped pipe tables (#53632) - #54302

Open
youngting520 wants to merge 1 commit into
NousResearch:mainfrom
youngting520:fix/telegram-collapsed-escaped-pipe-tables
Open

fix(telegram): repair model-collapsed/pre-escaped pipe tables (#53632)#54302
youngting520 wants to merge 1 commit into
NousResearch:mainfrom
youngting520:fix/telegram-collapsed-escaped-pipe-tables

Conversation

@youngting520

@youngting520 youngting520 commented Jun 28, 2026

Copy link
Copy Markdown

Summary

Repairs Telegram pipe tables that arrive with both of these model-generated defects:

  • structural pipes are pre-escaped (\| Date \| Event \|); and
  • multiple table rows are collapsed onto one physical line.

The repair runs before Telegram chooses the rich or legacy delivery path, and also runs when a streaming edit is finalized. Well-formed GFM tables, literal escaped pipes inside cells, prose, and code blocks remain unchanged.

This revision is rebased directly onto current main at 9712b8f0c. It keeps the adapter-local normalization boundary and preserves main's newer shared link-formatting extraction.

Issue status

Addresses #53632.

The reporter closed that issue after confirming the documented workaround—requesting plain GFM with one row per line and no pre-escaped structural pipes. The underlying adapter defect was not fixed by that workaround, and a later comment confirmed the same reproduction in a regular Telegram DM outside cron delivery. This PR provides the product-level fix.

Root cause

The existing Telegram paths only recognize a GFM table when the separator is on its own line. A collapsed payload therefore bypasses both rich-table routing and the legacy table-to-bullets converter. Generic MarkdownV2 escaping then escapes the already escaped bars again, exposing literal \| text to the recipient.

A separate but related problem exists in the shared table realigner: split_table_row() uses plain str.split("|"), which treats a legal escaped pipe inside a cell as a column boundary.

Fix

  • Detect a contiguous, outer-pipe-bounded candidate block before deciding whether to transform it.
  • Normalize only when every pipe is unambiguously escaped exactly once, the separator is uniquely in row two, and all rows have the same column count.
  • Transform the whole candidate block or leave it byte-for-byte unchanged; mixed or multiply escaped shapes are never partially rewritten.
  • Reconstruct collapsed rows using the separator's column count, including empty cells.
  • Make agent.markdown_tables.split_table_row() respect Markdown backslash parity: an odd run escapes a literal pipe, while an even run leaves a structural delimiter.
  • Re-escape literal pipe cell content when rendering a horizontal table.
  • Protect backtick fences, tilde fences, unclosed fences, and indented code.
  • Apply normalization in TelegramAdapter.send() and finalized edit_message() delivery, while leaving intermediate plain-text previews unchanged.

The repaired table feeds the existing rich renderer or legacy bullet conversion; this does not add another rendering path.

Review feedback addressed

The original implementation unconditionally changed every \| inside a bounded row. In response to review, this revision separates pre-escaped structure recognition from legal escaped-pipe cell parsing and adds rich, legacy, Discord, final-edit, ambiguity, atomicity, protected-code, and backslash-parity regressions.

Files changed

  • agent/markdown_tables.py — escaped-pipe-aware row splitting and safe horizontal re-rendering.
  • plugins/platforms/telegram/adapter.py — conservative table normalization before rich/legacy routing and on finalized edits.
  • tests/agent/test_markdown_tables.py — shared parser and realigner contracts.
  • tests/gateway/test_table_helpers.py — legacy table conversion coverage.
  • tests/gateway/test_telegram_format.py — normalization, legacy delivery, final-edit, protected-region, ambiguity, and atomicity coverage.
  • tests/gateway/test_telegram_rich_messages.py — rich delivery regressions.
  • tests/gateway/test_discord_format.py — downstream escaped-pipe regression coverage.

Validation

6 focused files: 155 passed, 0 failed
ruff check: passed
git diff --check: passed

The focused run covers Telegram formatting and delivery, Telegram rich messages and rich-newline handling, shared markdown-table behavior, legacy table helpers, and Discord formatting.

Risk

The normalizer is deliberately conservative. It requires a complete, structurally consistent table and performs an atomic whole-block conversion. Ambiguous escaping, inconsistent widths, misplaced or duplicate separators, valid literal-pipe cells, prose, and protected code regions are preserved unchanged.

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

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Competing fix for #53632 alongside #53661. This PR normalizes collapsed/pre-escaped pipe tables in the Telegram adapter (plugins/platforms/telegram/adapter.py send()); #53661 fixes the same symptom in core gateway/delivery.py (DeliveryRouter) but bundles unrelated changes (#53175 cleanup + docker CI gating) and needs branch isolation. Different code sites / mechanisms, so related — not a duplicate. Also relates to the broader open rich-table cluster #45327 (preserve raw pipe syntax) and #49258 (convert to native HTML). Flagging for a maintainer to pick the canonical layer.

@rodriguez46p-ui

Copy link
Copy Markdown

Hourly commander review on head ee51f161a: focused Telegram table checks look good; I did not find a blocker in this pass.

What I verified locally in a detached worktree:

  • python -m compileall -q plugins/platforms/telegram/adapter.py tests/gateway/test_telegram_format.py
  • python -m pytest tests/gateway/test_telegram_format.py tests/gateway/test_table_helpers.py tests/gateway/test_telegram_rich_messages.py tests/gateway/test_telegram_rich_newlines.py -q -o 'addopts=' ✅ (215 passed)
  • python -m ruff check plugins/platforms/telegram/adapter.py tests/gateway/test_telegram_format.py
  • Ad-hoc reproduction using the issue's linked calendar payload ✅:
    • _normalize_pipe_table_markdown(...) re-splits the collapsed escaped row into header/separator/data lines.
    • _needs_rich_rendering(...) changes from False before normalization to True after normalization.
    • Legacy table conversion no longer leaves \| in the stripped user-visible text and preserves link text like Fed Beige Book.

gh pr checks 54302 --repo NousResearch/hermes-agent currently reports no hosted checks for the branch, so this is local-only evidence.

@youngting520

Copy link
Copy Markdown
Author

Thanks for the automated triage and verification passes above. A few notes to help with the "canonical layer" decision:

Why this lives in the adapter send(). Both cron-scheduled output and ordinary interactive replies funnel through TelegramAdapter.send() before the rich/legacy split, so normalizing there fixes the symptom for every delivery path in one place rather than only the route a given caller happens to use. The reporter saw the same breakage when re-sending via the main agent, so a path-specific fix would leave that case broken.

It repairs the input shape, not just the escaping. The root payload is both pre-escaped (\|) and collapsed onto a single physical line, so the GFM separator row never sits on its own line and the existing table detection never fires. This re-splits it back into real multi-line GFM rows and un-escapes the bars, which flips _needs_rich_rendering() from False to True — so the table renders as a native Telegram table wherever rich messages are enabled, and as bullet groups only on the legacy fallback.

How this relates to the open rich-table work. This change is an input normalization step, not another renderer. #45327 (preserve raw pipe syntax on the rich path) and #49258 (convert pipe tables to native HTML) both operate on an already-well-formed table and reuse the existing separator-on-its-own-line detection — so a collapsed/pre-escaped payload like the one in #53632 isn't recognized by either of them today. This PR repairs that input shape before the rich/legacy split, which is what lets those native-rendering paths actually receive a table to render. It composes with either of them rather than overlapping, so it can stand alone or land underneath whichever rendering layer a maintainer settles on.

Scope. Two files, no unrelated changes: one normalizer plus a single call site, then it hands off to the existing table renderer (no second rendering path). It's idempotent and a no-op for any content without a pipe-bounded table row — prose, arithmetic, and fenced code are left verbatim, with negative-control tests covering those.

Happy to rebase onto or fold this into whichever layer is chosen as canonical.

@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 collapsed/pre-escaped delivery shape. Current main still has the reported detection gap: rich routing requires a separator on its own line (plugins/platforms/telegram/adapter.py:1405), and the legacy converter requires that separator to be the next line (gateway/platforms/helpers.py:386-396).

Problems

  • plugins/platforms/telegram/adapter.py:300 unconditionally turns every \| into | within a pipe-bounded row. \| is also valid GFM syntax for a literal pipe inside a cell. On the raw rich path, send() forwards that altered markdown before format_message() (plugins/platforms/telegram/adapter.py:3592; _rich_message_payload() at :1511-1518), changing cell boundaries and potentially losing content.

Suggested changes

  • Detect an all-structural pre-escaped table shape before unescaping; preserve literal-pipe cell escapes.
  • Add a regression for a well-formed table containing a \| cell value on both rich and legacy paths.

Automated hermes-sweeper review.

Comment thread plugins/platforms/telegram/adapter.py Outdated
@alt-glitch alt-glitch added comp/plugins Plugin system and bundled plugins and removed comp/gateway Gateway runner, session dispatch, delivery labels Jul 15, 2026
@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 15, 2026
@youngting520
youngting520 force-pushed the fix/telegram-collapsed-escaped-pipe-tables branch from ee51f16 to 87f0ba9 Compare July 16, 2026 15:47
@alt-glitch alt-glitch added P3 Low — cosmetic, nice to have and removed P2 Medium — degraded but workaround exists sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Jul 16, 2026
@youngting520
youngting520 force-pushed the fix/telegram-collapsed-escaped-pipe-tables branch from 87f0ba9 to 9a2192b Compare July 26, 2026 15:29
@alt-glitch alt-glitch added comp/gateway Gateway runner, session dispatch, delivery needs-decision Awaiting maintainer decision before any implementation P2 Medium — degraded but workaround exists platform/discord Discord bot adapter comp/plugins Plugin system and bundled plugins and removed comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have platform/discord Discord bot adapter labels Jul 26, 2026
@youngting520
youngting520 force-pushed the fix/telegram-collapsed-escaped-pipe-tables branch from 9a2192b to 85f4d28 Compare July 31, 2026 19:14
@alt-glitch alt-glitch removed the comp/gateway Gateway runner, session dispatch, delivery label Jul 31, 2026
@alt-glitch alt-glitch added the comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint label Jul 31, 2026
@youngting520
youngting520 force-pushed the fix/telegram-collapsed-escaped-pipe-tables branch from 85f4d28 to 062ce34 Compare August 4, 2026 18:04
…#53632)

Normalize only structurally verified pre-escaped tables, preserve valid escaped-pipe cells across rich and legacy delivery, and apply the same repair to finalized streaming edits. Keep fenced and indented code untouched and reject collapsed tables whose separator is not the second row.
@youngting520
youngting520 force-pushed the fix/telegram-collapsed-escaped-pipe-tables branch from 062ce34 to 8781dee Compare August 4, 2026 18:05
@alt-glitch alt-glitch added comp/gateway Gateway runner, session dispatch, delivery and removed comp/plugins Plugin system and bundled plugins labels Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/gateway Gateway runner, session dispatch, delivery needs-decision Awaiting maintainer decision before any implementation P2 Medium — degraded but workaround exists platform/telegram Telegram bot adapter sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants