Skip to content

fix(telegram): reuse preview message across tool-use rounds to prevent inter-tool text leak#32890

Closed
Sid-Qin wants to merge 1 commit into
openclaw:mainfrom
Sid-Qin:fix/inter-tool-text-leak-telegram-32535
Closed

fix(telegram): reuse preview message across tool-use rounds to prevent inter-tool text leak#32890
Sid-Qin wants to merge 1 commit into
openclaw:mainfrom
Sid-Qin:fix/inter-tool-text-leak-telegram-32535

Conversation

@Sid-Qin

@Sid-Qin Sid-Qin commented Mar 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Problem: When an LLM response involved multiple tool-use rounds, each intermediate text block between tool calls was delivered as a separate Telegram message. The user saw 3+ messages instead of just the final reply.
  • Why it matters: Intermediate text blocks are transient LLM narration, not intended for the user. They clutter the chat and create a poor experience, especially on slower devices.
  • What changed: Removed the archive + forceNewMessage logic in onAssistantMessageStart (bot-message-dispatch.ts). All tool-use rounds now share the same streaming preview message. Intermediate text is overwritten by the next round's text, and only the final reply is visible.
  • What did NOT change (scope boundary): Final delivery of multi-message replies, reasoning lane splitting, media delivery, and error handling are untouched. The forceNewMessage call for reasoning lane splitting remains.

Change Type (select all)

  • Bug fix

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

User-visible / Behavior Changes

  • During multi-tool-call turns, the user sees one updating preview message instead of multiple independent messages per tool-use round. Only the final reply text persists.

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No (fewer Telegram API calls since no preview archiving/deletion)
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • OS: macOS / Linux
  • Runtime: Node.js 22+

Steps

  1. Configure a Telegram channel with streaming enabled
  2. Send a message that triggers multiple sequential tool calls with text narration between them
  3. Observe the chat during processing

Expected

  • One preview message that updates, then the final reply

Actual

  • Before: Multiple separate messages (one per tool-use round), visible to the user before cleanup
  • After: Single preview message overwritten by each round, only final reply visible

Evidence

  • Failing test/log before + passing after

Updated 2 existing tests and verified all 49 tests pass.

Human Verification (required)

  • Verified scenarios: Multi-tool-call turns with text narration, single-message turns, media-only turns, error finals
  • Edge cases checked: First assistant message start (no previous output), finalized preview not re-archived, reasoning lane splitting unaffected
  • What you did not verify: Pi-level slow tool execution timing (unit test only)

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No

Failure Recovery (if this breaks)

  • How to disable/revert: Revert this commit to restore the archive + forceNewMessage behavior
  • Files/config to restore: src/telegram/bot-message-dispatch.ts

Risks and Mitigations

  • Risk: Multi-message assistant turns where each message should genuinely be a separate Telegram message may now share a preview
    • Mitigation: The final delivery path is unchanged and still creates separate messages for each final payload; only the streaming preview is affected

…t text leak

When an LLM response involved multiple tool-use rounds, onAssistantMessageStart
archived the current streaming preview and called forceNewMessage() to create a
new one for each round. Each round's intermediate text became an independent
visible Telegram message before the finally block could delete them.

The fix removes the archive+forceNewMessage logic so all tool-use rounds share
the same preview message. Intermediate text is overwritten by the next round,
and only the final reply text is visible to the user.

Closes openclaw#32535

Made-with: Cursor
@greptile-apps

greptile-apps Bot commented Mar 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a UX regression (#32535) where multiple separate Telegram messages were sent for intermediate LLM narration text during multi-tool-call turns. The fix is targeted and correct: the onAssistantMessageStart callback no longer archives the current preview and calls forceNewMessage(), so all tool-use rounds share a single streaming preview message. Intermediate text is overwritten in place and only the final reply remains visible.

Key observations:

  • The production change in bot-message-dispatch.ts is clean and minimal. Final delivery paths, reasoning lane splitting, media handling, and error handling are unaffected.
  • A stale comment in updateDraftFromPartial still references forceNewMessage as the reason hasStreamedMessage is tracked; the comment should describe its actual use for finalization tracking and reasoning lane splitting.
  • The updated multi-message test removes assertions for Message B and C finals. The test should be strengthened with an explicit assertion that deliverReplies is called for those messages, which route through a different delivery path after the first final finalizes the preview.

All 49 tests pass. The changes are safe to merge once the two identified items are addressed: clarifying the stale comment and strengthening the test coverage.

Confidence Score: 4/5

  • Safe to merge with the two identified improvements: updating a stale comment and strengthening test assertions for delivery paths.
  • The production change is clean and narrowly scoped, removing only the archive + forceNewMessage logic while preserving all other delivery paths. All 49 tests pass. The concerns are minor: one stale comment about field purpose and one test that should explicitly verify how subsequent finals are delivered after the first final is routed. Neither introduces runtime risk, but addressing them improves code clarity and test robustness.
  • src/telegram/bot-message-dispatch.ts (stale comment) and src/telegram/bot-message-dispatch.test.ts (test coverage strengthening)

Last reviewed commit: 7c87c67

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7c87c67431

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// forceNewMessage(), creating a visible message per round.
resetDraftLaneState(answerLane);
// New assistant message boundary: this lane now tracks a fresh preview lifecycle.
finalizedPreviewByLane.answer = false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep finalized preview flag when reusing same message

Now that assistant rounds reuse a single preview message, resetting finalizedPreviewByLane.answer on every onAssistantMessageStart can delete an already-finalized user reply on a later media-only round. In the sequence “text final (preview edited) -> assistant restart -> media-only final”, cleanup treats the answer lane as unfinalized and calls stream.clear(), and clear() deletes the current preview message ID, so the previously finalized text disappears from chat.

Useful? React with 👍 / 👎.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

2 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +475 to 482
// First final edits the single preview message; remaining finals are delivered normally.
expect(editMessageTelegram).toHaveBeenNthCalledWith(
1,
123,
1001,
"Message A final",
expect.any(Object),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The test verifies that the first final edits the preview, but it doesn't assert how Message B and C finals are delivered. The code comment claims they are "delivered normally" but without an explicit assertion, a future regression where those finals are accidentally dropped would pass silently.

After Message A final, finalizedPreviewByLane.answer becomes true, routing subsequent finals through deliverReplies instead of edit-via-preview. Consider adding an assertion to guard against silent regressions:

Suggested change
// First final edits the single preview message; remaining finals are delivered normally.
expect(editMessageTelegram).toHaveBeenNthCalledWith(
1,
123,
1001,
"Message A final",
expect.any(Object),
);
// First final edits the single preview message; remaining finals are delivered normally.
expect(editMessageTelegram).toHaveBeenNthCalledWith(
1,
123,
1001,
"Message A final",
expect.any(Object),
);
// Messages B and C finals should be delivered via deliverReplies since the
// single preview was already finalized by Message A.
expect(deliverReplies).toHaveBeenCalled();
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/telegram/bot-message-dispatch.test.ts
Line: 475-482

Comment:
The test verifies that the first final edits the preview, but it doesn't assert how Message B and C finals are delivered. The code comment claims they are "delivered normally" but without an explicit assertion, a future regression where those finals are accidentally dropped would pass silently.

After Message A final, `finalizedPreviewByLane.answer` becomes true, routing subsequent finals through `deliverReplies` instead of edit-via-preview. Consider adding an assertion to guard against silent regressions:

```suggestion
    // First final edits the single preview message; remaining finals are delivered normally.
    expect(editMessageTelegram).toHaveBeenNthCalledWith(
      1,
      123,
      1001,
      "Message A final",
      expect.any(Object),
    );
    // Messages B and C finals should be delivered via deliverReplies since the
    // single preview was already finalized by Message A.
    expect(deliverReplies).toHaveBeenCalled();
```

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

@greptile-apps

greptile-apps Bot commented Mar 3, 2026

Copy link
Copy Markdown
Contributor
Additional Comments (1)

src/telegram/bot-message-dispatch.ts
The comment references "forceNewMessage decision" but this field is no longer used for that purpose after this PR's changes. It's now used for finalization tracking (checked on line 527) and reasoning lane splitting (line 610). Update the comment to reflect its actual purpose.

    // Mark that we've received streaming content for finalization tracking.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/telegram/bot-message-dispatch.ts
Line: 271

Comment:
The comment references "forceNewMessage decision" but this field is no longer used for that purpose after this PR's changes. It's now used for finalization tracking (checked on line 527) and reasoning lane splitting (line 610). Update the comment to reflect its actual purpose.

```suggestion
    // Mark that we've received streaming content for finalization tracking.
```

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

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: XS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Inter-tool-call text blocks leak as separate messages (Telegram streaming preview)

2 participants