Deduplicate Telegram partial preview final replies#82625
Conversation
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>
|
Codex review: needs real behavior proof before merge. Summary 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 Next step before merge Security Review findings
Review detailsBest 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 Full review comments:
Overall correctness: patch is incorrect Acceptance criteria:
What I checked:
Likely related people:
Remaining risk / open question:
Codex review notes: model gpt-5.5, reasoning high; reviewed against 06e85d5eafdc. |
|
Updated #82625 with a fresh deterministic proof image and gate-friendly proof section. The proof compares
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 |
|
Reporting a production regression from this PR on SymptomOn 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 Root cause (matches what Codex review flagged on this PR)The Codex bot's review on this PR explicitly called this out — Concretely in 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
When the model emits a 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, Why the PR's own gating doesn't save Telegram block/progress modesThe PR description says "so Telegram partial streaming does not resend already-previewed blocks when block streaming is disabled" — but The Codex review preview-channel concern also extends to Slack: same Suggested fixEither:
For now I'm running Happy to dig deeper if a maintainer wants logs or repro steps. |
|
@jetd1 thank you for reporting. Landing a fix for this! |
…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>
…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>
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Fixes #82329.
Summary
runReplyAgent().Real behavior proof
streaming.mode="partial"could preview assistant text throughonPartialReply, 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.upstream/mainand PR head15d9d500cd45b8b1db50d90d13fdd62c17cbdb76.upstream/mainandfork/fix-telegram-partial-final-dedupe-82329; in each worktree ranpnpm --silent exec tsx ./probe-telegram-partial-final-dedupe.mjs. The probe imported the realbuildReplyPayloads()seam used after Telegram partial-preview delivery, passedpreviewStreamedText="First block\n\nSecond block\n\nHere is the chart", and supplied final payloads forFirst block,Second block, unrelated short text3, and media captionHere is the chartwithmediaUrl=file:///tmp/chart.png.upstream/mainreturns both duplicate final text blocks and the duplicate media caption; PR head removes duplicate final text, keeps unrelated short final text3, keeps media, and strips the duplicate media caption text.Supplemental validation
src/auto-reply/reply/agent-runner.tsandsrc/auto-reply/reply/agent-runner-payloads.tsmade 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.tspnpm 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.tsgit diff --checkpnpm check:changed