diff --git a/extensions/telegram/src/format.test.ts b/extensions/telegram/src/format.test.ts
index edf9ded2113b..e4fdc9a86479 100644
--- a/extensions/telegram/src/format.test.ts
+++ b/extensions/telegram/src/format.test.ts
@@ -635,8 +635,19 @@ describe("markdownToTelegramHtml", () => {
expect(containsLoneSurrogate(output)).toBe(false);
});
- it("fails loudly when tag overhead leaves no room for text", () => {
- expect(() => splitTelegramHtmlChunks("x", 10)).toThrow(/tag overhead/i);
+ it("delivers content as plain text when tag overhead fills the chunk", () => {
+ const chunks = splitTelegramHtmlChunks("x", 10);
+ expect(chunks).toHaveLength(1);
+ expect(chunks[0]).toBe("x");
+ });
+
+ it("keeps later formatting balanced after dropping an oversized tag scope", () => {
+ const oversizedLink = `first`;
+ const chunks = splitTelegramHtmlChunks(`${oversizedLink}second`, 20);
+
+ expect(chunks).toEqual(["firstsecond"]);
+ expect(chunks.every((chunk) => chunk.length <= 20)).toBe(true);
+ expect(telegramHtmlToPlainTextFallback(chunks.join(""))).toBe("firstsecond");
});
it("does not split an astral char across the chunk boundary", () => {
diff --git a/extensions/telegram/src/format.ts b/extensions/telegram/src/format.ts
index 814c971ac0cc..5ff4dce0432f 100644
--- a/extensions/telegram/src/format.ts
+++ b/extensions/telegram/src/format.ts
@@ -1496,6 +1496,7 @@ export function splitTelegramHtmlChunks(
const chunks: string[] = [];
const openTags: TelegramHtmlTag[] = [];
+ const suppressedTagNames: string[] = [];
let current = "";
let currentBlockCount = 0;
let currentMediaCount = 0;
@@ -1523,9 +1524,13 @@ export function splitTelegramHtmlChunks(
normalizedLimit - current.length - buildTelegramHtmlCloseSuffixLength(openTags);
if (available <= 0) {
if (!chunkHasPayload) {
- throw new Error(
- `Telegram HTML chunk limit exceeded by tag overhead (limit=${normalizedLimit})`,
- );
+ // Preserve the matching closes separately when tag overhead alone
+ // fills a chunk. Dropping only this active scope keeps later tags
+ // balanced while the affected text degrades to plain HTML content.
+ suppressedTagNames.push(...openTags.map((tag) => tag.name));
+ openTags.length = 0;
+ resetCurrent();
+ continue;
}
flushCurrent();
continue;
@@ -1590,7 +1595,12 @@ export function splitTelegramHtmlChunks(
}
}
- current += rawTag;
+ const closesOpenTag = isClosing && openTags.some((tag) => tag.name === tagName);
+ const closesSuppressedTag =
+ isClosing && !closesOpenTag && popLastTagName(suppressedTagNames, tagName);
+ if (!closesSuppressedTag) {
+ current += rawTag;
+ }
if (isSelfClosing) {
chunkHasPayload = true;
}
diff --git a/extensions/telegram/src/telegram-outbound.test.ts b/extensions/telegram/src/telegram-outbound.test.ts
index dc726325e565..dafd980db88b 100644
--- a/extensions/telegram/src/telegram-outbound.test.ts
+++ b/extensions/telegram/src/telegram-outbound.test.ts
@@ -1,6 +1,7 @@
import { chunkMarkdownTextWithMode } from "openclaw/plugin-sdk/reply-chunking";
+import { sendTextMediaPayload } from "openclaw/plugin-sdk/reply-payload";
// Telegram tests cover telegram outbound plugin behavior.
-import { describe, expect, it } from "vitest";
+import { describe, expect, it, vi } from "vitest";
import { splitTelegramHtmlChunks } from "./format.js";
import { telegramOutbound } from "./outbound-adapter.js";
import { clearTelegramRuntime } from "./runtime.js";
@@ -56,6 +57,33 @@ describe("telegramPlugin outbound", () => {
expect(telegramOutbound.chunker?.(text, 4000)).toEqual([text]);
});
+ it("delivers bounded HTML through the shared payload path when tag overhead overflows", async () => {
+ clearTelegramRuntime();
+ const oversizedLink = `first`;
+ const text = `${oversizedLink}second`;
+ const sendTelegram = vi.fn().mockResolvedValue({ messageId: "tg-1", chatId: "12345" });
+
+ await sendTextMediaPayload({
+ channel: "telegram",
+ ctx: {
+ cfg: {},
+ to: "12345",
+ text: "",
+ payload: { text },
+ formatting: { parseMode: "HTML" },
+ deps: { sendTelegram },
+ },
+ adapter: telegramOutbound,
+ });
+
+ expect(sendTelegram).toHaveBeenCalledTimes(1);
+ expect(sendTelegram).toHaveBeenCalledWith(
+ "12345",
+ "firstsecond",
+ expect.objectContaining({ textMode: "html" }),
+ );
+ });
+
it("keeps astral characters whole at positive configured chunk limits", () => {
clearTelegramRuntime();