openclaw - ✅(Solved) Fix [Bug]: Telegram markdown-mode outbound text > 4096 chars is sent as a single message and rejected (regression of #67396) [1 pull requests, 2 comments, 3 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#75868Fetched 2026-05-02 05:28:43
View on GitHub
Comments
2
Participants
3
Timeline
5
Reactions
2
Author
Timeline (top)
commented ×2closed ×1cross-referenced ×1referenced ×1

sendMessageTelegram only invokes the existing chunk planner when textMode === "html"; the default markdown path sends the entire payload as a single sendMessage call, so any agent response > 4096 chars fails with 400: Bad Request: message is too long. Reproduced on 2026.4.29 (a448042). This is a regression of #67396, which was closed as "already implemented" but the closing review did not check the default-textMode branch.

Error Message

if (!text || !text.trim()) throw new Error("Message must be non-empty for Telegram sends");

  • #74321 / #74656 (delivery-queue permanent-error classification; complementary fix that mitigates the orphan-preview symptom)

Root Cause

#67396 was closed referencing the chunker's existence; the closing review (extensions/telegram/src/send.ts:751) checks buildChunkedTextPlan is defined but does not check whether the default branch invokes it. Because of this the bug is still live on the latest shipped release.

Fix Action

Fixed

PR fix notes

PR #75893: fix(telegram): chunk markdown-mode outbound text > 4096 chars

Description (problem / solution / changelog)

Summary

sendMessageTelegram only invoked chunk planner for textMode === "html"; the default markdown path sent entire payload as a single sendMessage call, causing > 4096 char messages to fail with:

400: Bad Request: message is too long

This is a regression of #67396 - the closing review checked that chunker exists but did not verify the default markdown branch invokes it.

Fix

Remove the textMode gate - both markdown and html modes now use sendChunkedText which calls:

buildChunkedTextPlan -> splitTelegramHtmlChunks(4000)

Test Plan

  • Code modification verified
  • Telegram send tests pass (blocked by module resolution in sandbox)
  • End-to-end: > 4096 char markdown payload splits into multiple sendMessage calls

Local Verification (from issue reporter)

End-to-end verified on local install: a 6064-char test payload produced 7 sequential sendMessage calls, message-id sequence 2096..2102, ~500 ms apart, all 200 OK, zero message is too long errors.

Fixes #75868

Refs: #67396, #42240

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • extensions/telegram/src/send.ts (modified, +7/-8)

Code Example

{
  "id": "f73b5c5c-487e-408a-b2fd-8a3ed33812a5",
  "enqueuedAt": 1777004716336,
  "channel": "telegram",
  "payloads": [
    { "text": "<17-char preview line>", "mediaUrls": [] },
    { "text": "<5840-char follow-up report>", "mediaUrls": [] }
  ],
  "retryCount": 5,
  "lastError": "Call to 'sendMessage' failed! (400: Bad Request: message is too long)"
}

---

2026-04-26T23:55:14 [delivery-recovery] Retry failed for delivery f73b5c5c... message is too long
2026-04-27T09:56:58 [delivery-recovery] Retry failed for delivery f73b5c5c... message is too long
2026-05-01T08:32:14 [delivery-recovery] Retry failed for delivery f73b5c5c... message is too long
2026-05-01T22:52:36 [delivery-recovery] Retry failed for delivery f73b5c5c... message is too long
2026-05-01T23:05:37 [delivery-recovery] Delivery f73b5c5c... exceeded max retries (5/5) — moving to failed/

---

const textMode = opts.textMode ?? "markdown";  // default
...
if (!text || !text.trim()) throw new Error("Message must be non-empty for Telegram sends");
let textResult;
if (textMode === "html") textResult = await sendChunkedText(text, "text send");  // chunks
else textResult = await sendTelegramTextChunks([{                                  // does NOT chunk
    plainText: opts.plainText ?? text,
    htmlText: renderHtmlText(text)
}], "text send");

---

-    if (textMode === "html") textResult = await sendChunkedText(text, "text send");
-    else textResult = await sendTelegramTextChunks([{
-        plainText: opts.plainText ?? text,
-        htmlText: renderHtmlText(text)
-    }], "text send");
+    textResult = await sendChunkedText(text, "text send");
RAW_BUFFERClick to expand / collapse

Summary

sendMessageTelegram only invokes the existing chunk planner when textMode === "html"; the default markdown path sends the entire payload as a single sendMessage call, so any agent response > 4096 chars fails with 400: Bad Request: message is too long. Reproduced on 2026.4.29 (a448042). This is a regression of #67396, which was closed as "already implemented" but the closing review did not check the default-textMode branch.

Steps to reproduce

  1. Send a Telegram direct message that triggers a > 4096-char assistant response (e.g. summarize a long thread, deep-research output, or any multi-paragraph report). No special config — default textMode.
  2. Watch ~/.openclaw/logs/gateway.err.log for [delivery-recovery] Retry failed... message is too long.
  3. Check ~/.openclaw/delivery-queue/failed/ for the stuck envelope.

Expected behavior

Outbound markdown text > 4096 chars is split into <= 4000-char chunks (the existing splitTelegramHtmlChunks / plain-text fallback chunker that the HTML branch already uses) and delivered as multiple sequential sendMessage calls. This matches the merged behavior for textMode === "html" (#42240) and the documented channels.telegram.textChunkLimit = 4000 semantics.

Actual behavior

Markdown payloads > 4096 chars are sent as one chunk and rejected by Telegram. The send is then enqueued for retry, where the same oversized payload fails 5 more times before landing in failed/. End-user impact: the long content never arrives.

When the failing envelope contains multiple payloads (e.g. a short preview line + a long report), the short payload succeeds on every retry while the long one keeps failing — so the user receives the same orphan preview line repeatedly across days, with no follow-up content. From the user perspective this looks like a "ghost message" that keeps reappearing.

OpenClaw version

2026.4.29 (a448042)

Operating system

macOS 25.2.0 / Apple Silicon M4

Install method

npm global (/opt/homebrew/lib/node_modules/openclaw)

Model

n/a — bug is in the outbound channel layer, reproduces independent of model

Provider / routing chain

agent → openclaw gateway → telegram bot api

Logs, screenshots, and evidence

Failing envelope from ~/.openclaw/delivery-queue/failed/:

{
  "id": "f73b5c5c-487e-408a-b2fd-8a3ed33812a5",
  "enqueuedAt": 1777004716336,
  "channel": "telegram",
  "payloads": [
    { "text": "<17-char preview line>", "mediaUrls": [] },
    { "text": "<5840-char follow-up report>", "mediaUrls": [] }
  ],
  "retryCount": 5,
  "lastError": "Call to 'sendMessage' failed! (400: Bad Request: message is too long)"
}

gateway.err.log retry chain across 9 days:

2026-04-26T23:55:14 [delivery-recovery] Retry failed for delivery f73b5c5c... message is too long
2026-04-27T09:56:58 [delivery-recovery] Retry failed for delivery f73b5c5c... message is too long
2026-05-01T08:32:14 [delivery-recovery] Retry failed for delivery f73b5c5c... message is too long
2026-05-01T22:52:36 [delivery-recovery] Retry failed for delivery f73b5c5c... message is too long
2026-05-01T23:05:37 [delivery-recovery] Delivery f73b5c5c... exceeded max retries (5/5) — moving to failed/

The relevant code path in extensions/telegram/src/send.ts (paraphrased from the shipped dist/extensions/telegram/send-t4GTTdHF.js):

const textMode = opts.textMode ?? "markdown";  // default
...
if (!text || !text.trim()) throw new Error("Message must be non-empty for Telegram sends");
let textResult;
if (textMode === "html") textResult = await sendChunkedText(text, "text send");  // chunks
else textResult = await sendTelegramTextChunks([{                                  // does NOT chunk
    plainText: opts.plainText ?? text,
    htmlText: renderHtmlText(text)
}], "text send");

The chunk planner (buildChunkedTextPlansplitTelegramHtmlChunks(rawText, 4000)) exists and is exercised by tests, but is only reachable from the html branch. The default markdown branch wraps the entire raw text into a single sendTelegramTextChunks([single]) call, which sends it as one api.sendMessage payload.

#67396 was closed referencing the chunker's existence; the closing review (extensions/telegram/src/send.ts:751) checks buildChunkedTextPlan is defined but does not check whether the default branch invokes it. Because of this the bug is still live on the latest shipped release.

#42240 (merged 2026-03-10) explicitly carved out scope: "What did NOT change: markdown-mode plain-text fallback/retry behavior" — so the markdown-mode hole was a known unaddressed scope at merge time, not a regression. #67396 then closed assuming it was addressed.

Local fix that resolves it

Removed the textMode gate so markdown also invokes sendChunkedText:

-    if (textMode === "html") textResult = await sendChunkedText(text, "text send");
-    else textResult = await sendTelegramTextChunks([{
-        plainText: opts.plainText ?? text,
-        htmlText: renderHtmlText(text)
-    }], "text send");
+    textResult = await sendChunkedText(text, "text send");

End-to-end verified on the local install: a 6064-char test payload produced 7 sequential sendMessage calls, message-id sequence 2096..2102, ~500 ms apart, all 200 OK, zero message is too long errors, zero retries.

Impact and severity

  • Affected: any Telegram direct/group target receiving > 4096-char assistant text on the default markdown path. Long reports, deep-research output, summaries, multi-paragraph code reviews — all silently fail.
  • Severity: High. End-user-visible content loss. Compounded by interaction with #74321 (delivery-queue retry classifier) — when the failing envelope contains a sibling shorter payload that does succeed, the user receives orphan preview lines on every retry tick across days, looking like a ghost message.
  • Frequency: every payload > 4096 chars on default markdown. Reproducible 100% on 2026.4.29.
  • Consequence: missed assistant output (Telegram users) + confusing repeated orphan messages for users running gateways with multi-payload sends.

Additional information

Cross-references:

  • #67396 (closed as implemented; see this issue for evidence the close was incorrect — but #67396 is locked, hence this fresh report)
  • #42240 (merged predecessor that explicitly excluded the markdown branch from scope)
  • #64717 (open PR — different bug, makes textChunkLimit config respected; doesn't address the textMode-gate hole)
  • #74321 / #74656 (delivery-queue permanent-error classification; complementary fix that mitigates the orphan-preview symptom)

Happy to send a PR — the change is small and the test surface is extensions/telegram/src/send.test.ts.

extent analysis

TL;DR

Remove the textMode gate in extensions/telegram/src/send.ts to invoke sendChunkedText for both "html" and "markdown" modes.

Guidance

  • Identify the textMode conditional in extensions/telegram/src/send.ts and remove the gate to always invoke sendChunkedText.
  • Verify the fix by sending a test payload > 4096 characters and checking for multiple sequential sendMessage calls in the logs.
  • Review the extensions/telegram/src/send.test.ts tests to ensure the fix does not introduce any regressions.
  • Consider submitting a PR with the fix, as the change is small and the test surface is well-defined.

Example

The local fix provided in the issue body can be used as a starting point:

-    if (textMode === "html") textResult = await sendChunkedText(text, "text send");
-    else textResult = await sendTelegramTextChunks([{
-        plainText: opts.plainText ?? text,
-        htmlText: renderHtmlText(text)
-    }], "text send");
+    textResult = await sendChunkedText(text, "text send");

Notes

The fix assumes that the sendChunkedText function is correctly implemented and tested. Additionally, the fix may have implications for other parts of the codebase, such as the delivery-queue retry classifier.

Recommendation

Apply the workaround by removing the textMode gate and invoking sendChunkedText for both "html" and "markdown" modes, as this fix has been end-to-end verified on a local install.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

Outbound markdown text > 4096 chars is split into <= 4000-char chunks (the existing splitTelegramHtmlChunks / plain-text fallback chunker that the HTML branch already uses) and delivered as multiple sequential sendMessage calls. This matches the merged behavior for textMode === "html" (#42240) and the documented channels.telegram.textChunkLimit = 4000 semantics.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix [Bug]: Telegram markdown-mode outbound text > 4096 chars is sent as a single message and rejected (regression of #67396) [1 pull requests, 2 comments, 3 participants]