Skip to content

Telegram draft streaming fails with TEXTDRAFT_PEER_INVALID - fix with editMessageText #7803

@Arlo83963

Description

@Arlo83963

Telegram Draft Streaming Fix

Problem

The current Telegram draft streaming implementation uses sendMessageDraft API, which returns TEXTDRAFT_PEER_INVALID error for regular bots in private chats.

{"ok":false,"error_code":400,"description":"Bad Request: TEXTDRAFT_PEER_INVALID"}

Additionally, the canStreamDraft condition in bot-message-dispatch.js requires:

  1. typeof resolvedThreadId === "number" - but private chats return undefined
  2. resolveBotTopicsEnabled(primaryCtx) - but most bots don't have topics enabled

This causes streaming to be disabled for most private chat scenarios.

Solution

1. Replace sendMessageDraft with sendMessage + editMessageText

Instead of using the unreliable sendMessageDraft API, use the standard approach:

  1. Send initial message with sendMessage
  2. Update content progressively with editMessageText
  3. Add a cursor indicator during streaming
  4. Remove cursor on final update

2. Simplify canStreamDraft condition

Change from:

const canStreamDraft = streamMode !== "off" &&
    isPrivateChat &&
    typeof resolvedThreadId === "number" &&
    (await resolveBotTopicsEnabled(primaryCtx));

To:

const canStreamDraft = streamMode !== "off" &&
    isPrivateChat;

3. Pass final text to stop()

Ensure the complete final text is passed to stop() to prevent truncation:

await draftStream?.stop(payload.text);

Files Changed

dist/telegram/draft-stream.js

const TELEGRAM_DRAFT_MAX_CHARS = 4096;
const DEFAULT_THROTTLE_MS = 400;
export function createTelegramDraftStream(params) {
    const maxChars = Math.min(params.maxChars ?? TELEGRAM_DRAFT_MAX_CHARS, TELEGRAM_DRAFT_MAX_CHARS);
    const throttleMs = Math.max(100, params.throttleMs ?? DEFAULT_THROTTLE_MS);
    const chatId = params.chatId;
    const threadParams = typeof params.messageThreadId === "number"
        ? { message_thread_id: Math.trunc(params.messageThreadId) }
        : undefined;
    let lastSentText = "";
    let lastSentAt = 0;
    let pendingText = "";
    let inFlight = false;
    let timer;
    let stopped = false;
    let messageId = null;
    let initialSent = false;
    let fullText = "";

    const sendOrEdit = async (text, isFinal = false) => {
        if (stopped && !isFinal)
            return;
        const trimmed = text.trimEnd();
        if (!trimmed)
            return;
        const displayText = trimmed.length > maxChars 
            ? trimmed.slice(0, maxChars - 3) + "..."
            : trimmed;
        const textWithCursor = isFinal ? displayText : displayText + " ▌";
        if (textWithCursor === lastSentText && !isFinal)
            return;
        lastSentText = textWithCursor;
        lastSentAt = Date.now();
        try {
            if (!initialSent) {
                const result = await params.api.sendMessage(chatId, textWithCursor, {
                    ...threadParams,
                });
                messageId = result.message_id;
                initialSent = true;
            } else if (messageId) {
                await params.api.editMessageText(chatId, messageId, textWithCursor);
            }
        }
        catch (err) {
            const errMsg = err instanceof Error ? err.message : String(err);
            if (errMsg.includes("message is not modified")) {
                return;
            }
            params.warn?.(`telegram edit stream error: ${errMsg}`);
        }
    };
    
    // ... flush, schedule, update functions remain similar ...
    
    const stop = async (finalText) => {
        stopped = true;
        if (timer) {
            clearTimeout(timer);
            timer = undefined;
        }
        const textToFinalize = finalText || fullText || pendingText || lastSentText;
        pendingText = "";
        while (inFlight) {
            await new Promise(resolve => setTimeout(resolve, 50));
        }
        if (messageId && textToFinalize) {
            try {
                const cleanText = textToFinalize.replace(/ $/, "");
                await sendOrEdit(cleanText, true);
            } catch (err) {
                params.warn?.(`telegram edit stream: failed to finalize: ${err}`);
            }
        }
    };
    
    const getMessageId = () => messageId;
    return { update, flush, stop, getMessageId };
}

dist/telegram/bot-message-dispatch.js

- const canStreamDraft = streamMode !== "off" &&
-     isPrivateChat &&
-     typeof resolvedThreadId === "number" &&
-     (await resolveBotTopicsEnabled(primaryCtx));
+ const canStreamDraft = streamMode !== "off" &&
+     isPrivateChat;
  if (streamedMessageId) {
-     await draftStream?.stop();
+     await draftStream?.stop(payload.text);
      deliveryState.delivered = true;
      return;
  }

Testing

Tested with Telegram bot in private chat:

  • ✅ Streaming displays progressively with cursor
  • ✅ Final message contains complete content
  • ✅ No duplicate messages sent

Environment

  • OpenClaw version: 2026.1.29
  • Node.js: v22.22.0
  • Telegram Bot API

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions