Skip to content

fix(telegram): skip redundant final edit in partial streaming mode#19479

Closed
v8hid wants to merge 1 commit intoopenclaw:mainfrom
v8hid:fix/telegram-duplicate-reply
Closed

fix(telegram): skip redundant final edit in partial streaming mode#19479
v8hid wants to merge 1 commit intoopenclaw:mainfrom
v8hid:fix/telegram-duplicate-reply

Conversation

@v8hid
Copy link
Copy Markdown

@v8hid v8hid commented Feb 17, 2026

Summary

  • Problem: In draft streaming, Telegram finalization would attempt editMessageText with unchanged content.
  • Why it matters: Telegram rejects same-content edits (message is not modified), which trigger failover cleanup/send and create duplicate visible messages plus error logs.
  • What changed: Added lastAppliedText() tracking in draft-stream.ts and used it in bot-message-dispatch.ts to short-circuit unchanged final edits when no mutation is needed.
  • Kept final edit behavior for real mutations (formatting pass, inline buttons, linkPreview=false) and expanded tests.

Rebased cleanly onto main — turns out merging main into a feature branch makes the bot think you're trying to smuggle 200 unrelated commits into your PR. Lesson learned. 2 clean commits now, as intended.

Replaces #17766 (auto-closed by bot after a merge commit polluted the history).

Change Type

  • Bug fix

Scope

  • Integrations

Related Fix

Commit ac2ede5 by @steipete already treats message is not modified API errors as success. This PR adds a complementary optimization that prevents the unnecessary API call from being attempted in the first place.

Both fixes work together:

  • ac2ede5bb: Reactive safety net at the API error handling layer
  • This PR: Proactive optimization at the logic layer to avoid the no-op API call entirely

Greptile Summary

This PR adds a proactive optimization to skip the redundant editMessageText Telegram API call when the preview stream already shows the exact final text, complementing the existing reactive error-handling safety net.

The approach is sound — adding lastSentText() to TelegramDraftStream and checking it in bot-message-dispatch.ts before attempting the final edit. The skip conditions correctly account for formatting passes (renderTelegramHtmlText comparison), inline button mutations, and linkPreview=false mutations.

Key issue found:

  • lastSentText is assigned optimistically before the API call (line 78 of draft-stream.ts). When editMessageText or sendMessage throws, lastSentText already reflects the attempted text, not the last successfully delivered text. This contradicts the test assertion added in draft-stream.test.ts ("keeps lastSentText at the last successful value when an edit fails") — that test will fail against the current implementation.
  • More critically, this creates a correctness gap in the dispatch optimization: if a mid-stream edit fails (e.g. network error), lastSentText equals the failed-attempt text. If the final text also equals that value, the final edit is skipped, leaving the Telegram message showing the last successfully delivered (older) content rather than the final text.
  • The fix is straightforward: save the previous lastSentText value before the assignment, and restore it in the catch block (and in the sendMessage early-return path).

Everything else looks good:

  • The needsFormattingPass check using renderTelegramHtmlText is correct and matches how editMessageTelegram renders text internally.
  • The hasFinalEditMutation check for buttons and linkPreview correctly identifies cases requiring the final edit.
  • Test coverage in bot-message-dispatch.test.ts is comprehensive (plain text match, formatting pass, buttons, linkPreview).
  • The lastSentText: () => string | undefined type is slightly wider than the actual "" initialization (always returns string), but this is harmless.

Confidence Score: 3/5

  • Not safe to merge as-is: the new draft-stream test will fail, and the optimization has a correctness gap when a streaming edit fails mid-stream.
  • The lastSentText variable is set before the API call (optimistic update), but the test and the dispatch optimization require it to reflect the last confirmed delivery. A failing streaming edit leaves lastSentText pointing to the undelivered text, allowing the final edit to be incorrectly skipped. The new draft-stream.test.ts test ("keeps lastSentText at the last successful value when an edit fails") will fail against the current implementation. Fixing it requires restoring lastSentText to its prior value in the catch block.
  • src/telegram/draft-stream.ts — the sendOrEditStreamMessage function needs to restore lastSentText on API failure.

Last reviewed commit: 1999e63

(4/5) You can add custom instructions or style guidelines for the agent here!

@openclaw-barnacle openclaw-barnacle bot added channel: telegram Channel integration: telegram size: S labels Feb 17, 2026
Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 17, 2026

Additional Comments (1)

src/telegram/draft-stream.ts
lastSentText updated before the API call succeeds

lastSentText = trimmed is assigned on line 78, before entering the try block. When editMessageText throws (caught at line 93), lastSentText already reflects the attempted text rather than the last confirmed-delivered text, and stopped = true is set.

This matters for the skip-edit optimization in bot-message-dispatch.ts: if a streaming edit fails mid-stream (network hiccup, etc.), lastSentText will equal the text of the failed attempt, even though the Telegram message still shows the prior successful text. When the final text also equals that failed-attempt text, the optimization skips the final edit — leaving the visible message outdated.

The new test added in draft-stream.test.ts ("keeps lastSentText at the last successful value when an edit fails") asserts stream.lastSentText() === "Hello" after a failed edit to "Hello again", which will fail against this implementation.

Fix: save the previous value and restore it on catch, or move the assignment after the successful call.

    const prevLastSentText = lastSentText;
    lastSentText = trimmed;
    try {
      if (typeof streamMessageId === "number") {
        await params.api.editMessageText(chatId, streamMessageId, trimmed);
        return true;
      }
      const sent = await params.api.sendMessage(chatId, trimmed, replyParams);
      const sentMessageId = sent?.message_id;
      if (typeof sentMessageId !== "number" || !Number.isFinite(sentMessageId)) {
        stopped = true;
        params.warn?.("telegram stream preview stopped (missing message id from sendMessage)");
        lastSentText = prevLastSentText;
        return false;
      }
      streamMessageId = Math.trunc(sentMessageId);
      return true;
    } catch (err) {
      stopped = true;
      lastSentText = prevLastSentText;
      params.warn?.(
        `telegram stream preview failed: ${err instanceof Error ? err.message : String(err)}`,
      );
      return false;
    }
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/telegram/draft-stream.ts
Line: 78:99

Comment:
**`lastSentText` updated before the API call succeeds**

`lastSentText = trimmed` is assigned on line 78, before entering the `try` block. When `editMessageText` throws (caught at line 93), `lastSentText` already reflects the attempted text rather than the last confirmed-delivered text, and `stopped = true` is set.

This matters for the skip-edit optimization in `bot-message-dispatch.ts`: if a streaming edit fails mid-stream (network hiccup, etc.), `lastSentText` will equal the text of the failed attempt, even though the Telegram message still shows the prior successful text. When the final text also equals that failed-attempt text, the optimization skips the final edit — leaving the visible message outdated.

The new test added in `draft-stream.test.ts` (`"keeps lastSentText at the last successful value when an edit fails"`) asserts `stream.lastSentText()` === `"Hello"` after a failed edit to `"Hello again"`, which will fail against this implementation.

Fix: save the previous value and restore it on catch, or move the assignment after the successful call.

```suggestion
    const prevLastSentText = lastSentText;
    lastSentText = trimmed;
    try {
      if (typeof streamMessageId === "number") {
        await params.api.editMessageText(chatId, streamMessageId, trimmed);
        return true;
      }
      const sent = await params.api.sendMessage(chatId, trimmed, replyParams);
      const sentMessageId = sent?.message_id;
      if (typeof sentMessageId !== "number" || !Number.isFinite(sentMessageId)) {
        stopped = true;
        params.warn?.("telegram stream preview stopped (missing message id from sendMessage)");
        lastSentText = prevLastSentText;
        return false;
      }
      streamMessageId = Math.trunc(sentMessageId);
      return true;
    } catch (err) {
      stopped = true;
      lastSentText = prevLastSentText;
      params.warn?.(
        `telegram stream preview failed: ${err instanceof Error ? err.message : String(err)}`,
      );
      return false;
    }
```

How can I resolve this? If you propose a fix, please make it concise.

@openclaw-barnacle
Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle bot added the stale Marked as stale due to inactivity label Feb 28, 2026
@v8hid v8hid force-pushed the fix/telegram-duplicate-reply branch 2 times, most recently from 5c2221e to 156b16a Compare March 1, 2026 00:27
Instead of trying to edit a preview message with identical content, finalize
the existing preview in place. Editing with the same content can return a
Telegram API error, which then triggers failover cleanup/send and may
duplicate output.

- Move lastSentText/lastSentParseMode updates to after successful API call
  so failed edits do not corrupt tracked last-sent state
- Expose lastSentText() on TelegramDraftStream
- Guard editPreview in bot-message-dispatch to skip the final edit when the
  rendered text already matches what the draft stream last sent, and no
  markup mutation (buttons, linkPreview) is required
@v8hid v8hid force-pushed the fix/telegram-duplicate-reply branch from fda4f9a to c1df1fb Compare March 1, 2026 01:06
@v8hid
Copy link
Copy Markdown
Author

v8hid commented Mar 1, 2026

This pull request has been automatically marked as stale due to inactivity. Please add updates or it will be closed.

updated. ready to merge.

@openclaw-barnacle openclaw-barnacle bot removed the stale Marked as stale due to inactivity label Mar 17, 2026
@openclaw-barnacle
Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle bot added stale Marked as stale due to inactivity and removed stale Marked as stale due to inactivity labels Mar 23, 2026
@openclaw-barnacle
Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle bot added the stale Marked as stale due to inactivity label Mar 30, 2026
@openclaw-barnacle
Copy link
Copy Markdown

Closing due to inactivity.
If you believe this PR should be revived, post in #pr-thunderdome-dangerzone on Discord to talk to a maintainer.
That channel is the escape hatch for high-quality PRs that get auto-closed.

@openclaw-barnacle openclaw-barnacle bot closed this Apr 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: telegram Channel integration: telegram size: S stale Marked as stale due to inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant