Skip to content

Deduplicate Telegram partial preview final replies#82625

Merged
giodl73-repo merged 3 commits into
openclaw:mainfrom
giodl73-repo:fix-telegram-partial-final-dedupe-82329
May 17, 2026
Merged

Deduplicate Telegram partial preview final replies#82625
giodl73-repo merged 3 commits into
openclaw:mainfrom
giodl73-repo:fix-telegram-partial-final-dedupe-82329

Conversation

@giodl73-repo

@giodl73-repo giodl73-repo commented May 16, 2026

Copy link
Copy Markdown
Contributor

Fixes #82329.

Summary

  • Track the latest partial-preview text produced during runReplyAgent().
  • Suppress final text payloads that exactly match the already streamed preview snapshot or its blank-line-delimited blocks.
  • Preserve safety cases: errors still deliver, unrelated final text still delivers, and media still delivers while duplicate caption text is stripped.

Real behavior proof

  • Behavior or issue addressed: Telegram streaming.mode="partial" could preview assistant text through onPartialReply, then send the same final assistant blocks again when block streaming was disabled. The fix dedupes final text already covered by partial preview streaming while keeping unrelated short text and media payloads.
  • Real environment tested: WSL Ubuntu-24.04 source worktrees created from upstream/main and PR head 15d9d500cd45b8b1db50d90d13fdd62c17cbdb76.
  • Exact steps or command run after this patch: created clean detached worktrees for upstream/main and fork/fix-telegram-partial-final-dedupe-82329; in each worktree ran pnpm --silent exec tsx ./probe-telegram-partial-final-dedupe.mjs. The probe imported the real buildReplyPayloads() seam used after Telegram partial-preview delivery, passed previewStreamedText="First block\n\nSecond block\n\nHere is the chart", and supplied final payloads for First block, Second block, unrelated short text 3, and media caption Here is the chart with mediaUrl=file:///tmp/chart.png.
  • Evidence after fix (screenshot, recording, terminal capture, console output, redacted runtime log, linked artifact, or copied live output):

PR #82625 Telegram partial preview final dedupe proof

Fresh proof artifact: https://raw.githubusercontent.com/giodl73-repo/openclaw/proof-artifacts/pr-82625-fresh/pr-82625/pr-82625-telegram-partial-final-dedupe-before-after-proof.png
Proof summary: https://raw.githubusercontent.com/giodl73-repo/openclaw/proof-artifacts/pr-82625-fresh/pr-82625/summary.txt

BEFORE upstream/main:
status=0
payloadCount=4
duplicateFinalCount=2
shortFinalText=3
mediaKept=true
mediaText=Here is the chart
texts=First block|Second block|3|Here is the chart

AFTER PR #82625 head:
status=0
payloadCount=2
duplicateFinalCount=0
shortFinalText=3
mediaKept=true
mediaText=<missing>
texts=3|<missing>
  • Observed result after fix: upstream/main returns both duplicate final text blocks and the duplicate media caption; PR head removes duplicate final text, keeps unrelated short final text 3, keeps media, and strips the duplicate media caption text.
  • What was not tested: no live Telegram bot account or hosted Telegram delivery was used; this is source-level runtime proof of the shared final-payload construction seam used after partial-preview delivery.

Supplemental validation

  • Before/after regression proof: reverting only src/auto-reply/reply/agent-runner.ts and src/auto-reply/reply/agent-runner-payloads.ts made the new payload and e2e regressions fail, then restoring the fix made them pass.
  • CI=1 node scripts/run-vitest.mjs src/auto-reply/reply/agent-runner-payloads.test.ts src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts
  • pnpm exec oxfmt --check --threads=1 src/auto-reply/reply/agent-runner.ts src/auto-reply/reply/agent-runner-payloads.ts src/auto-reply/reply/agent-runner-payloads.test.ts src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts
  • git diff --check
  • pnpm check:changed

Track the latest partial-preview reply text during reply-agent runs and suppress matching final text-only payloads so Telegram partial streaming does not resend already-previewed blocks when block streaming is disabled.

Keep the dedupe exact-match based to avoid dropping unrelated short finals, preserve errors, and keep unsent media while stripping duplicate caption text.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@openclaw-barnacle openclaw-barnacle Bot added size: S maintainer Maintainer-authored PR labels May 16, 2026
@clawsweeper

clawsweeper Bot commented May 16, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge.

Summary
The PR records the latest onPartialReply text in runReplyAgent(), passes it to buildReplyPayloads(), and filters matching final text/captions with regression tests.

Reproducibility: yes. for source-level reproduction: current main's no-block/no-direct-key branch returns final payloads unchanged, and the contributor proof shows duplicate final text/caption payloads before the patch. I did not establish live Telegram reproduction in this read-only review.

Real behavior proof
Needs stronger real behavior proof before merge: Source-level before/after screenshot proof is present, but this visible Telegram streaming change still needs live Telegram proof showing the preview finalizes without duplicate final bubbles. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, ask a maintainer to comment @clawsweeper re-review.

Next step before merge
Human review is needed because the blocker is the generic Telegram/Slack preview-finalization contract plus missing live Telegram proof, not a safe isolated repair task.

Security
Cleared: The diff only changes TypeScript reply-payload logic/tests and adds no dependency, workflow, script, package, or credential-handling changes.

Review findings

  • [P2] Scope preview dedupe to finalizable preview channels — src/auto-reply/reply/agent-runner-payloads.ts:432-434
Review details

Best possible solution:

Carry an explicit Telegram or finalizable-preview signal into payload building, or move the dedupe into the Telegram finalization path, so duplicate Telegram finals are suppressed without removing final payloads required by Slack or other preview finalizers.

Do we have a high-confidence way to reproduce the issue?

Yes for source-level reproduction: current main's no-block/no-direct-key branch returns final payloads unchanged, and the contributor proof shows duplicate final text/caption payloads before the patch. I did not establish live Telegram reproduction in this read-only review.

Is this the best way to solve the issue?

No: the current PR dedupes every onPartialReply user in generic payload construction, but Slack also uses that hook and needs the final payload to commit its draft preview. A channel-scoped or explicit finalizable-preview contract is the safer fix.

Full review comments:

  • [P2] Scope preview dedupe to finalizable preview channels — src/auto-reply/reply/agent-runner-payloads.ts:432-434
    previewStreamedText now makes buildReplyPayloads() drop final text for any channel whose onPartialReply fired. Slack draft previews also use onPartialReply, but their final payload is what runs deliverWithFinalizableLivePreviewAdapter() and marks the draft as committed; dropping it can leave the preview unfinalized. Gate this on Telegram or an explicit finalizable-preview contract.
    Confidence: 0.88

Overall correctness: patch is incorrect
Overall confidence: 0.88

Acceptance criteria:

  • Live Telegram proof with channels.telegram.streaming.mode: "partial" and block streaming off, showing the preview finalizes without duplicate final bubbles.
  • CI=1 node scripts/run-vitest.mjs src/auto-reply/reply/agent-runner-payloads.test.ts src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts
  • Slack preview-finalization regression coverage if the fix remains in generic reply-payload construction.

What I checked:

Likely related people:

  • Peter Steinberger: Current blame/log history for the reply-payload dedupe branch, Slack preview finalizer, and shared live-message finalizer points to recent work in the affected core/channel paths. (role: recent area contributor; confidence: high; commits: ffc7bda44375, 80eeb688c1c2, 842e6f16437c; files: src/auto-reply/reply/agent-runner-payloads.ts, src/auto-reply/reply/agent-runner.ts, extensions/slack/src/monitor/message-handler/dispatch.ts)
  • sahilsatralkar: Related Telegram preview-finalization work in commit 3064ea7 touched the current Telegram lane-delivery path and tests for incomplete preview finalization. (role: adjacent Telegram preview-finalization contributor; confidence: medium; commits: 3064ea78ab65; files: extensions/telegram/src/lane-delivery-text-deliverer.ts, extensions/telegram/src/lane-delivery.test.ts, extensions/telegram/src/config-ui-hints.ts)
  • OpenCils: Older Telegram duplicate-message work fixed DM draft streaming finalization before the Telegram code moved into the current extension layout, so this is useful historical context but less direct for the current files. (role: earlier Telegram duplicate-preview contributor; confidence: low; commits: 3fe4c1930588; files: src/telegram/lane-delivery.ts, src/telegram/lane-delivery.test.ts, src/telegram/bot-message-dispatch.ts)

Remaining risk / open question:

  • No live Telegram transport proof is present yet for the visible chat behavior.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 06e85d5eafdc.

@clawsweeper clawsweeper Bot added the mantis: telegram-visible-proof Mantis should capture Telegram visible proof. label May 16, 2026
@giodl73-repo

Copy link
Copy Markdown
Contributor Author

Updated #82625 with a fresh deterministic proof image and gate-friendly proof section.

The proof compares upstream/main against PR head 15d9d500cd45b8b1db50d90d13fdd62c17cbdb76 using the real buildReplyPayloads() seam:

  • Before: duplicate final text blocks are returned after partial preview, and the duplicate media caption is still present.
  • After: duplicate final text is removed, unrelated short text 3 remains, media remains, and duplicate media caption text is stripped.

Fresh proof image: https://raw.githubusercontent.com/giodl73-repo/openclaw/proof-artifacts/pr-82625-fresh/pr-82625/pr-82625-telegram-partial-final-dedupe-before-after-proof.png
Proof summary: https://raw.githubusercontent.com/giodl73-repo/openclaw/proof-artifacts/pr-82625-fresh/pr-82625/summary.txt

@clawsweeper clawsweeper Bot added the P1 High-priority user-facing bug, regression, or broken workflow. label May 16, 2026
@clawsweeper clawsweeper Bot added the impact:message-loss Channel message delivery can be lost, duplicated, or misrouted. label May 17, 2026
@giodl73-repo giodl73-repo merged commit bd51d8f into openclaw:main May 17, 2026
116 checks passed
@jetd1

jetd1 commented May 17, 2026

Copy link
Copy Markdown
Contributor

Reporting a production regression from this PR on main (commit bd51d8f2dd).

Symptom

On Telegram DMs, the final assistant reply is streamed into a draft preview, then deleted with a fade animation the moment the turn completes. Only the reasoning trace (if any) remains. The user-visible final message is lost entirely.

Reproducible on agents.blockStreamingDefault unset (i.e. default "off") + any channels.telegram.streaming.mode that creates an answer-draft stream ("partial", "block", "progress"). Reverting this commit on top of current main makes the bug disappear cleanly.

Root cause (matches what Codex review flagged on this PR)

The Codex bot's review on this PR explicitly called this out — "Overall correctness: patch is incorrect, confidence 0.88", with the P2 finding "Scope preview dedupe to finalizable preview channels". The PR was merged without addressing it.

Concretely in src/auto-reply/reply/agent-runner-payloads.ts:432-443 after this PR:

shouldDropFinalPayloads
  ? preserveUnsentMediaAfterBlockStream(...)
  : params.blockStreamingEnabled
    ? // filter via blockReplyPipeline.hasSentPayload
    : params.directlySentBlockKeys?.size
      ? // filter via block content key
      : previewStreamedText.size > 0
        ? suppressPreviewStreamedPayloads(dedupedPayloads)   // ← drops the final text payload
        : dedupedPayloads

previewStreamedText is populated by agent-runner.ts:1280-1292, which wraps onPartialReply and records the last streamed text segment as-is. The Telegram draft lane is wired through this exact onPartialReply (extensions/telegram/src/bot-message-dispatch.ts:1591-1598), and it fires whenever answerLane.stream || reasoningLane.stream exists — i.e. for every non-off Telegram preview mode, not just partial.

When the model emits a [thinking, text] final turn that exactly matches the last preview chunk (which is the normal case for short/medium replies), suppressPreviewStreamedPayloads strips the final text payload entirely. Then in bot-message-dispatch.ts:1773-1777:

if (lane.finalized) {
  await stream.stop();   // keeps the message
} else {
  await stream.clear();  // deletes the draft
}

Because the final payload never reached the delivery path, lane.finalized stays false, so stream.clear() deletes the draft message the user was just watching. End result: nothing visible except the reasoning trace.

Why the PR's own gating doesn't save Telegram block/progress modes

The PR description says "so Telegram partial streaming does not resend already-previewed blocks when block streaming is disabled" — but blockStreamingEnabled in buildReplyPayloads is the agent-level agents.blockStreamingDefault flag, not the channel-level channels.telegram.streaming.mode. Almost no one sets blockStreamingDefault: "on" (it's an opt-in agent default). So in practice this dedup branch runs for the vast majority of configurations that have any Telegram preview enabled, not the narrow "partial streaming, agent block off" slice the PR description targets.

The Codex review preview-channel concern also extends to Slack: same onPartialReply hook, same risk of dropping the final payload that deliverWithFinalizableLivePreviewAdapter() needs to commit the draft. I didn't verify the Slack symptom in production, but the code path is identical.

Suggested fix

Either:

  1. Revert and rethink the dedup contract. Telegram's draft-preview finalization already replaces the draft with the final text in-place; double-bubble shouldn't happen if the draft lane is healthy. If there's a specific narrow case where it does, fix that case directly in the Telegram draft finalizer rather than at the channel-agnostic buildReplyPayloads layer.
  2. Gate the new branch on an explicit finalizable-preview contract / channel signal (as the Codex review suggested), so Telegram block/progress and Slack drafts are not affected. The current branch unconditionally trusts that any onPartialReply consumer doesn't need the final payload — which is false.

For now I'm running git revert bd51d8f2dd on my deploy branch and final delivery is back to normal.

Happy to dig deeper if a maintainer wants logs or repro steps.

@obviyus

obviyus commented May 18, 2026

Copy link
Copy Markdown
Contributor

@jetd1 thank you for reporting. Landing a fix for this!

Elarwei001 added a commit to Elarwei001/openclaw that referenced this pull request May 18, 2026
…enclaw#80520)

The previous core-layer dedup (PR openclaw#82625 / commit bd51d8f, reverted
in the preceding commit) suppressed preview-streamed final payloads in
src/auto-reply/reply/agent-runner-payloads.ts but had no path to also
signal lane.finalized on the channel side. When every final was
suppressed (e.g., embedded harness + reasoning-model paragraph-split
delivery), the Telegram lane stayed unfinalized and the end-of-turn
clearUnfinalizedStream cleanup deleted the preview message — users
saw the reply briefly and then watched it disappear (openclaw#80520).

This change moves the dedup to the channel layer so the
"skip duplicate send" and "mark lane finalized" steps happen
atomically:

- Adds extensions/telegram/src/preview-dedup.ts with the same
  normalization and per-block dedup semantics that the previous
  core-layer suppressPreviewStreamedPayloads used. Helpers are
  unit-tested in preview-dedup.test.ts (13 cases).

- In extensions/telegram/src/bot-message-dispatch.ts, the dispatcher's
  deliver callback now checks each text-only final against the answer
  lane's lastPartialText (whole + per-block normalized). On match, the
  lane is marked finalized in the same step that skips the send,
  keeping preview/finalize state coupled.

- Guards the dedup with a previewMessageId check: only suppresses when
  the preview actually has an established Telegram message. If the
  preview never landed, the final is still delivered through
  sendPayload so the user receives something. This avoids an
  additional latent issue in the core-layer dedup, which had no
  channel state at all.

- Payloads with media keep their existing path through
  lane-delivery-text-deliverer.ts; the hasMedia branch there already
  strips duplicate captions while keeping the media.

Adds 4 integration tests in bot-message-dispatch.test.ts covering
the dedup path, the previewMessageId guard, multi-block deliveries,
and error preservation. No existing tests modified.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Elarwei001 added a commit to Elarwei001/openclaw that referenced this pull request May 19, 2026
…enclaw#80520)

The previous core-layer dedup (PR openclaw#82625 / commit bd51d8f, reverted
in the preceding commit) suppressed preview-streamed final payloads in
src/auto-reply/reply/agent-runner-payloads.ts but had no path to also
signal lane.finalized on the channel side. When every final was
suppressed (e.g., embedded harness + reasoning-model paragraph-split
delivery), the Telegram lane stayed unfinalized and the end-of-turn
clearUnfinalizedStream cleanup deleted the preview message — users
saw the reply briefly and then watched it disappear (openclaw#80520).

This change moves the dedup to the channel layer so the
"skip duplicate send" and "mark lane finalized" steps happen
atomically:

- Adds extensions/telegram/src/preview-dedup.ts with the same
  normalization and per-block dedup semantics that the previous
  core-layer suppressPreviewStreamedPayloads used. Helpers are
  unit-tested in preview-dedup.test.ts (13 cases).

- In extensions/telegram/src/bot-message-dispatch.ts, the dispatcher's
  deliver callback now checks each text-only final against the answer
  lane's lastPartialText (whole + per-block normalized). On match, the
  lane is marked finalized in the same step that skips the send,
  keeping preview/finalize state coupled.

- Guards the dedup with a previewMessageId check: only suppresses when
  the preview actually has an established Telegram message. If the
  preview never landed, the final is still delivered through
  sendPayload so the user receives something. This avoids an
  additional latent issue in the core-layer dedup, which had no
  channel state at all.

- Payloads with media keep their existing path through
  lane-delivery-text-deliverer.ts; the hasMedia branch there already
  strips duplicate captions while keeping the media.

Adds 4 integration tests in bot-message-dispatch.test.ts covering
the dedup path, the previewMessageId guard, multi-block deliveries,
and error preservation. No existing tests modified.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
galiniliev pushed a commit to galiniliev/openclaw that referenced this pull request May 20, 2026
Track the latest partial-preview reply text during reply-agent runs and suppress matching final text-only payloads so Telegram partial streaming does not resend already-previewed blocks when block streaming is disabled.

Keep the dedupe exact-match based to avoid dropping unrelated short finals, preserve errors, and keep unsent media while stripping duplicate caption text.
SebTardif pushed a commit to SebTardif/openclaw that referenced this pull request May 24, 2026
Track the latest partial-preview reply text during reply-agent runs and suppress matching final text-only payloads so Telegram partial streaming does not resend already-previewed blocks when block streaming is disabled.

Keep the dedupe exact-match based to avoid dropping unrelated short finals, preserve errors, and keep unsent media while stripping duplicate caption text.
SebTardif pushed a commit to SebTardif/openclaw that referenced this pull request May 24, 2026
Track the latest partial-preview reply text during reply-agent runs and suppress matching final text-only payloads so Telegram partial streaming does not resend already-previewed blocks when block streaming is disabled.

Keep the dedupe exact-match based to avoid dropping unrelated short finals, preserve errors, and keep unsent media while stripping duplicate caption text.
SebTardif pushed a commit to SebTardif/openclaw that referenced this pull request May 24, 2026
Track the latest partial-preview reply text during reply-agent runs and suppress matching final text-only payloads so Telegram partial streaming does not resend already-previewed blocks when block streaming is disabled.

Keep the dedupe exact-match based to avoid dropping unrelated short finals, preserve errors, and keep unsent media while stripping duplicate caption text.
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
Track the latest partial-preview reply text during reply-agent runs and suppress matching final text-only payloads so Telegram partial streaming does not resend already-previewed blocks when block streaming is disabled.

Keep the dedupe exact-match based to avoid dropping unrelated short finals, preserve errors, and keep unsent media while stripping duplicate caption text.
galiniliev pushed a commit to galiniliev/openclaw that referenced this pull request May 25, 2026
Track the latest partial-preview reply text during reply-agent runs and suppress matching final text-only payloads so Telegram partial streaming does not resend already-previewed blocks when block streaming is disabled.

Keep the dedupe exact-match based to avoid dropping unrelated short finals, preserve errors, and keep unsent media while stripping duplicate caption text.
SebTardif pushed a commit to SebTardif/openclaw that referenced this pull request May 26, 2026
Track the latest partial-preview reply text during reply-agent runs and suppress matching final text-only payloads so Telegram partial streaming does not resend already-previewed blocks when block streaming is disabled.

Keep the dedupe exact-match based to avoid dropping unrelated short finals, preserve errors, and keep unsent media while stripping duplicate caption text.
SebTardif pushed a commit to SebTardif/openclaw that referenced this pull request May 26, 2026
Track the latest partial-preview reply text during reply-agent runs and suppress matching final text-only payloads so Telegram partial streaming does not resend already-previewed blocks when block streaming is disabled.

Keep the dedupe exact-match based to avoid dropping unrelated short finals, preserve errors, and keep unsent media while stripping duplicate caption text.
SebTardif pushed a commit to SebTardif/openclaw that referenced this pull request May 26, 2026
Track the latest partial-preview reply text during reply-agent runs and suppress matching final text-only payloads so Telegram partial streaming does not resend already-previewed blocks when block streaming is disabled.

Keep the dedupe exact-match based to avoid dropping unrelated short finals, preserve errors, and keep unsent media while stripping duplicate caption text.
jameslcowan pushed a commit to jameslcowan/openclaw that referenced this pull request Jun 2, 2026
Track the latest partial-preview reply text during reply-agent runs and suppress matching final text-only payloads so Telegram partial streaming does not resend already-previewed blocks when block streaming is disabled.

Keep the dedupe exact-match based to avoid dropping unrelated short finals, preserve errors, and keep unsent media while stripping duplicate caption text.
SYU8384 pushed a commit to SYU8384/openclaw that referenced this pull request Jun 3, 2026
Track the latest partial-preview reply text during reply-agent runs and suppress matching final text-only payloads so Telegram partial streaming does not resend already-previewed blocks when block streaming is disabled.

Keep the dedupe exact-match based to avoid dropping unrelated short finals, preserve errors, and keep unsent media while stripping duplicate caption text.
sablehead pushed a commit to sablehead/openclaw that referenced this pull request Jun 10, 2026
Track the latest partial-preview reply text during reply-agent runs and suppress matching final text-only payloads so Telegram partial streaming does not resend already-previewed blocks when block streaming is disabled.

Keep the dedupe exact-match based to avoid dropping unrelated short finals, preserve errors, and keep unsent media while stripping duplicate caption text.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

impact:message-loss Channel message delivery can be lost, duplicated, or misrouted. maintainer Maintainer-authored PR mantis: telegram-visible-proof Mantis should capture Telegram visible proof. P1 High-priority user-facing bug, regression, or broken workflow. size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Telegram: duplicate final-reply blocks when streaming.mode="partial" and blockStreaming is off (regression in 5.12)

3 participants