From 0382ca218a8a8993023f0712913dde2f66a54b6a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 21 Aug 2026 17:56:22 -0700 Subject: [PATCH] perf(telegram): reuse prepared rich message plan (#127719) Co-authored-by: Amp --- extensions/telegram/src/rich-message.ts | 32 ++++++++++++------- .../telegram/src/telegram-text-delivery.ts | 21 ++++++------ .../telegram/src/transport-payload.test.ts | 19 ++++++++++- 3 files changed, 48 insertions(+), 24 deletions(-) diff --git a/extensions/telegram/src/rich-message.ts b/extensions/telegram/src/rich-message.ts index 35c20230b3fe..ea7d262e3127 100644 --- a/extensions/telegram/src/rich-message.ts +++ b/extensions/telegram/src/rich-message.ts @@ -219,18 +219,28 @@ export function buildTelegramRichBlocksPlan( }; } -export function splitTelegramRichMessageTextChunks(params: { - text: string; - textLimit: number; - tableMode?: MarkdownTableMode; - skipEntityDetection?: boolean; -}): TelegramRichTextChunk[] { +export function splitTelegramRichMessageTextChunks( + params: + | { + text: string; + textLimit: number; + tableMode?: MarkdownTableMode; + skipEntityDetection?: boolean; + } + | { + plan: TelegramRichMessagePlan; + textLimit: number; + }, +): TelegramRichTextChunk[] { // Convert the full markdown document first so fences/tables stay intact, then // enforce block/char limits on the typed block list (including oversized pre). - const plan = buildTelegramRichMarkdownPlan(params.text, { - tableMode: params.tableMode, - skipEntityDetection: params.skipEntityDetection, - }); + const plan = + "plan" in params + ? params.plan + : buildTelegramRichMarkdownPlan(params.text, { + tableMode: params.tableMode, + skipEntityDetection: params.skipEntityDetection, + }); // The render already committed to the document-level linkify decision (a // skip anywhere disables our file-ref code-wrapping everywhere), so every // chunk must carry the same wire flag; re-deriving per chunk would let @@ -248,7 +258,7 @@ export function splitTelegramRichMessageTextChunks(params: { degradationReasons: index === 0 ? plan.degradationReasons : [], }; }); - if (chunked.length === 0 && params.text.trim()) { + if (chunked.length === 0 && "text" in params && params.text.trim()) { // Markdown that projects to zero blocks (e.g. link definitions only) must // still send readable source text instead of silently dropping the reply. const blocks: InputRichBlock[] = [{ type: "paragraph", text: params.text }]; diff --git a/extensions/telegram/src/telegram-text-delivery.ts b/extensions/telegram/src/telegram-text-delivery.ts index 2080c0756803..dbe1718c4bb8 100644 --- a/extensions/telegram/src/telegram-text-delivery.ts +++ b/extensions/telegram/src/telegram-text-delivery.ts @@ -106,18 +106,15 @@ export function planTelegramTextDeliveryPages( if (richPlan.richMessage.blocks.length === 0 && params.text.trim()) { return [plainPage(params.text)]; } - return splitTelegramRichMessageTextChunks({ - text: params.text, - textLimit: maxChars, - tableMode: params.tableMode, - skipEntityDetection: params.skipEntityDetection, - }).map((chunk) => ({ - plainText: chunk.plainText, - sourceText: chunk.plainText, - sourceTextMode: "markdown" as const, - richMessage: chunk.richMessage, - degradationReasons: chunk.degradationReasons, - })); + return splitTelegramRichMessageTextChunks({ plan: richPlan, textLimit: maxChars }).map( + (chunk) => ({ + plainText: chunk.plainText, + sourceText: chunk.plainText, + sourceTextMode: "markdown" as const, + richMessage: chunk.richMessage, + degradationReasons: chunk.degradationReasons, + }), + ); } if (params.textMode === "plain") { return splitTelegramPlainTextChunks(params.text, maxChars) diff --git a/extensions/telegram/src/transport-payload.test.ts b/extensions/telegram/src/transport-payload.test.ts index b35d491ffbae..8bf723b246dc 100644 --- a/extensions/telegram/src/transport-payload.test.ts +++ b/extensions/telegram/src/transport-payload.test.ts @@ -7,7 +7,7 @@ import { createPluginStateSyncKeyedStoreForTests, resetPluginStateStoreForTests, } from "openclaw/plugin-sdk/plugin-state-test-runtime"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createTelegramCallbackMessageActions } from "./bot-handlers.callback-actions.js"; import { buildTelegramMessageContextForTest } from "./bot-message-context.test-harness.js"; import { telegramPlugin } from "./channel.js"; @@ -23,6 +23,21 @@ import { sendTypingTelegram } from "./send-actions.js"; import { sendMessageTelegram } from "./send-message.js"; import { sendPollTelegram } from "./send-special.js"; +const richMarkdownProjection = vi.hoisted(() => ({ count: 0 })); + +vi.mock("./rich-blocks.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + markdownToTelegramRichBlocks: ( + ...args: Parameters + ) => { + richMarkdownProjection.count += 1; + return actual.markdownToTelegramRichBlocks(...args); + }, + }; +}); + type CapturedRequest = { body: Buffer; contentType: string; @@ -151,6 +166,7 @@ describe("Telegram topic transport payloads", () => { beforeEach(() => { requests.length = 0; + richMarkdownProjection.count = 0; resetPluginStateStoreForTests(); resetTelegramMessageCacheForTest(); installTelegramStateRuntimeForTest(); @@ -259,6 +275,7 @@ describe("Telegram topic transport payloads", () => { direct_messages_topic_id: DIRECT_TOPIC_ID, }); expect(request && parseJsonBody(request)).not.toHaveProperty("message_thread_id"); + expect(richMarkdownProjection.count).toBe(1); }); it("rejects poll and typing for channel Direct Messages without transport", async () => {