Skip to content

fix(telegram): distinguish and render streamed reasoning/commentary progress lanes#98907

Merged
obviyus merged 13 commits into
openclaw:mainfrom
Marvinthebored:fix/telegram-streamed-progress-lanes
Jul 3, 2026
Merged

fix(telegram): distinguish and render streamed reasoning/commentary progress lanes#98907
obviyus merged 13 commits into
openclaw:mainfrom
Marvinthebored:fix/telegram-streamed-progress-lanes

Conversation

@Marvinthebored

@Marvinthebored Marvinthebored commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

On Telegram, the streamed progress window (the live "thinking" preview shown while a reply is generated) was broken in several visible ways compared to Discord:

  1. Reasoning and commentary were indistinguishable. Both lanes rendered plain, with no 🧠/💬 markers — a user watching a streamed turn could not tell the model's reasoning from its commentary. Discord renders reasoning italic under 🧠 and commentary upright under 💬.
  2. Model Markdown leaked or was stripped. **bold**, backticks, and _…_ in lane text either surfaced as literal characters or were silently flattened to plain text.
  3. <br> broke every multi-line preview at the transport. Progress drafts join rendered lines with <br>, but Bot API parse_mode=HTML has no <br> tag. Telegram rejected every multi-line preview edit with "can't parse entities", and the parse-error fallback silently downgraded the whole draft to unformatted plain text — so streamed progress formatting effectively never rendered on non-rich accounts.
  4. No collapse summary. When the window tore down, it left nothing behind — the streamed activity vanished with no digest of what happened.
  5. Window churn / scroll-jump. The window was deleted and reposted on lane handovers, and on /reasoning on it repositioned below the durable reasoning block, yanking the client's scroll position up by pages and sometimes posting the final answer off-screen.
  6. Interim answer blocks streamed into the window. Partial answer content leaked into the progress preview (Discord suppresses this).

Why This Change Was Made

Channel-side only (extensions/telegram/**); no core src/** changes. One focused commit per concern:

  1. Lane identity + rendering — pass the 🧠/💬 lane prefixes and commentaryItalics:false to the shared progress-draft compositor (mirroring the Discord channel), and render lane bodies through renderTelegramHtmlText (the parse_mode=HTML-safe converter). Lane lines collapse to one line first (multi-line commentary otherwise forms block markdown — a \n\n---\n\n separator turns the paragraph above into a setext <h2>, which Telegram rejects). Long lines clip inside the whole-line _…_ wrapper so truncation cannot chop the closing underscore and degrade italic to plain.
  2. Transport — convert <br> joins to literal newlines in the parse_mode=HTML transport branch, matching what the rich-message branch already does.
  3. Preview lifetime — keep the streaming preview on screen ≥4s from first appearance before a teardown delete, scheduled detached (never awaited) so turn teardown is not stalled.
  4. Collapse summary bar — on teardown, collapse the window in place (edit, not delete) into a one-line activity digest: 🧠 N thoughts · 💬 N notes · 🛠️ N tool calls · ⏱️ Ns. Counts reflect only streamed content (durable /verbose on messages own their own delivery and are excluded — persistent-message XOR window-count invariant).
  5. Single stationary window — one Telegram message per turn, edited through every lane handover; zero happy-path deletes. The final answer is delivered before the bar edit so the window never repositions below durable content (kills the scroll-jump). Rare repositions post-new-then-delete-old (deferred delete) instead of delete-then-repost.
  6. Interim answer-block suppression — suppress partial answer blocks from the progress window (Discord parity).

Non-goals: final-answer rendering and the durable (/verbose on) lanes are untouched — this is only the streamed progress window.

User Impact

Telegram users watching a streamed reply now see a formatted, stable progress window: reasoning (🧠, italic) clearly distinct from commentary (💬, plain), the model's Markdown rendered (bold step names, monospace commands) instead of leaking or being stripped, a stationary box that no longer deletes/reposts or jumps the scroll, and a persistent one-line activity digest left behind on collapse. Biggest improvement for deepseek, which narrates progress almost entirely through its markdown-heavy reasoning lane.

Maintainer-ready classification

  • Fix classification: channel-local bug fix + small feature (collapse bar). Bounded to extensions/telegram/**; no core, no plugin-SDK, no config/default surface added, no new env var.
  • Root cause: the Telegram progress path never passed the compositor's per-channel lane markers, hand-escaped lane bodies instead of using the parse_mode-safe renderer, and emitted <br> into a parse_mode=HTML transport that has no such tag. The scroll-jump was a window-lifecycle defect (delete-then-repost across lane handovers).
  • Architecture / source-of-truth check: lane semantics come from the shared createChannelProgressDraftCompositor (src/channels/progress-draft-compositor.ts) — the same source Discord consumes; this change makes Telegram a consumer of that contract rather than diverging. The tool-count gate reuses the compositor's own public predicate isChannelProgressDraftWorkToolName (via openclaw/plugin-sdk/channel-outbound), so the bar's 🛠️ tally cannot drift from what the compositor actually renders.
  • Sibling-surface check: Discord is the mirror. Its summary bar (deliverDiscordReplysendDurableMessageBatch) has no transcript-mirror seam, so the Telegram bar was hardened to match (see F1 below) — the invariant now holds identically on both channels.
  • Patch quality: git diff --numstat net prod LOC is small and localized; new files are progress-summary.ts (tracker + formatter) and its test. No new fallback readers, no config compat.
  • Related-PR scan: rebased onto current main (post-2026.7.1) including b07fd6f1b4 fix(telegram): keep group history always on; the only overlap was an import block (dropped the now-deleted group-history-context import, kept upstream's selectTelegramGroupHistoryAfterLastSelf). No open PR touches the progress-window path.

Real behavior proof (required for external PRs)

  • Behavior addressed: streamed progress-window lane rendering, <br> transport, collapse summary bar, stationary-window (no scroll-jump), interim-answer suppression.

  • Real environment tested: dev gateway built from this exact branch head, Telegram Desktop, fresh session per turn (/new).

  • Exact steps: /reasoning stream + markdown-heavy prompt for the lane render; /reasoning on + /verbose off for the collapse-bar + stationary-window turn.

  • Evidence (runtime capture):

    Summary bar + lane gating (/reasoning on, /verbose off, gpt-5.5) — a single frame shows the collapse bar 💬 2 notes · 🛠️ 2 tool calls · ⏱️ 61s, the durable 🧠 reasoning block rendered italic with bold/monospace preserved, and the correct gating: durable (/reasoning on) reasoning renders in full but is not counted in the bar, while streamed notes/tool calls are — matching the persistent-message XOR window-count invariant.

    summary bar + lane gating

    Lane rendering close-ups (/reasoning stream, markdown-heavy prompt) — reasoning 🧠 italic with **bold** rendered bold-italic and backtick commands monospace, all inside the italic wrapper; commentary 💬 upright with bold step names and monospace commands (visual contrast with the italic reasoning):

    🧠 reasoning zoom · 💬 commentary zoom · full-stage: prompt · 🧠 reasoning · 💬 commentary · 💬 step 3

  • Observed result: 🧠 reasoning renders italic with bold/monospace preserved inside the wrapper; 💬 commentary renders upright; the window stays a single stationary message and collapses in place into 🧠 N thoughts · 💬 N notes · 🛠️ N tool calls · ⏱️ Ns; the final answer arrives before the bar with no scroll-jump.

  • 4s dwell / cleanup — terminal proof (this head): the streaming preview is kept on screen for MIN_PREVIEW_DWELL_MS = 4000 from first appearance before a detached cleanup delete (never before the dwell, never blocking teardown), asserted directly on the current head by extensions/telegram/src/draft-stream.test.ts:

    ✓ extensions/telegram/src/draft-stream.test.ts > createTelegramDraftStream >
      keeps the streaming preview on screen for the dwell window before deleting
    Test Files  1 passed (1)
    

    Complemented by CI's Mantis native Telegram Desktop before/after capture on this same head (Overall: true), which exercises the live preview → cleanup flow.

  • What was not tested: streamed reasoning-lane italics were exercised on a reasoning-bearing provider in an earlier build and by unit tests; the latest sanity turn used a model that emitted empty reasoning, so its streamed-🧠 italic frame is covered by the unit suite + the earlier live capture rather than the final sanity capture. Group DM / forum-topic progress windows were not separately re-captured (unaffected by this change).

  • Proof limitations: captures are from a dev gateway on a developer Telegram account (names/emails redacted), not a production deployment.

Red-team hardening (this branch)

An adversarial pass produced 5 findings, all fixed in fix(telegram): red-team hardening for the progress collapse bar:

  • F1 (HIGH) — transcript pollution. The bar was mirrored into the model's session transcript (durable:truetranscriptMirror), so the model could read its own cosmetic activity digest back as a prior turn. Discord's bar has no such seam. Fix: mirrorTranscript:false on the two bar sends; real finals keep mirroring.
  • F4 — tool overcount. The bar's 🛠️ counter incremented on non-work tools (message/reply/etc.) before the compositor's work-tool gate. Fix: gate the count with isChannelProgressDraftWorkToolName + toolProgress so the tally matches what the window renders.
  • F2 — silent collapse drop. finalizeToPreview ignored a failed edit (flood-wait 429). Fix: return undefined so the caller falls back to a durable bar post.
  • F3 — unguarded cosmetic send. A throw in the cleanup-fallback bar send could fail the turn. Fix: try/catch + logVerbose.
  • F5 (LOW) — ghost-preview race. A first send racing a reposition could orphan a bubble. Fix: repositionedSendGenerations guard deletes the late-landing send deferred.

Follow-up (ClawSweeper review finding): F3 originally guarded only the cleanup fallback bar send. The normal path (applyProgressCollapseSummary) awaited an unguarded durable send when finalizeToPreview could not edit in place, and sendPayload throws on delivery failure — so a cosmetic bar flood-wait could fail an otherwise-complete turn (merge-risk: message-delivery). Both cosmetic-bar sends now route through one shared guarded helper postCosmeticSummaryBar (swallow + logVerbose); added the regression for the no-live-message fallback send throwing while the final answer still delivers.

Tests and validation

  • node scripts/run-vitest.mjs run --config test/vitest/vitest.extension-telegram.config.ts extensions/telegram/src/{bot-message-dispatch,draft-stream,progress-summary}.test.ts239 passed (3 files). New coverage: lane markdown rendering in both lanes, clipped-long-reasoning stays italic, multi-line commentary stays parse_mode-safe (the setext-<h2> trap), <br>→newline transport, ≥4s dwell timing, collapse-bar counts/format, stationary-window reposition + deferred-delete, one test per red-team finding (F1 mirror-absent, F4 counts, F2 failed-edit fallback, F3 throw-swallow, F5 reposition-race), and both cosmetic-bar fallback sends throwing without failing the turn (cleanup + no-live-message paths).
  • Rebased clean onto current main (post-2026.7.1); telegram suites green on the rebased head.

🤖 Generated with Claude Code

@openclaw-barnacle openclaw-barnacle Bot added channel: telegram Channel integration: telegram size: S labels Jul 2, 2026
@clawsweeper

clawsweeper Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed July 2, 2026, 7:32 PM ET / 23:32 UTC.

Summary
The branch updates Telegram streamed progress preview rendering, draft transport/lifecycle, collapse-summary tracking, durable reasoning markers, and regression coverage under extensions/telegram.

PR surface: Source +664, Tests +1125. Total +1789 across 9 files.

Reproducibility: yes. source-level reproduction is high confidence: current main lacks the Telegram lane prefixes/markdown rendering and parse_mode HTML
normalization that the PR repairs. I did not run a live Telegram repro in this read-only review, but the PR includes Telegram Desktop proof and targeted regressions.

Review metrics: none identified.

Root-cause cluster
Relationship: fixed_by_candidate
Canonical: #90962
Summary: This PR is a broad candidate fix for the Telegram progress-window/commentary/tool-progress problem tracked by the canonical issue; the older narrower PR overlaps but is not a safer canonical replacement because it is conflicting and lacks proof.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🦞 diamond lobster
Proof: 🦞 diamond lobster ✨ media proof bonus
Patch quality: 🦞 diamond lobster
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Next step before merge

  • No ClawSweeper repair is needed; the remaining action is maintainer final review and landing judgment, including whether this PR supersedes the older conflicting candidate.

Security
Cleared: No concrete security or supply-chain concern was found; the diff stays in Telegram runtime/tests and avoids workflow, dependency, secret, lockfile, or package-publish changes.

Review details

Best possible solution:

Land the channel-local Telegram implementation after maintainer final review, then use the merged PR to resolve the related canonical issue and reassess the older conflicting candidate PR.

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

Yes, source-level reproduction is high confidence: current main lacks the Telegram lane prefixes/markdown rendering and parse_mode HTML
normalization that the PR repairs. I did not run a live Telegram repro in this read-only review, but the PR includes Telegram Desktop proof and targeted regressions.

Is this the best way to solve the issue?

Yes, this appears to be the best maintainable fix: it stays inside the Telegram plugin, consumes the shared progress compositor contract, and avoids new core/config surfaces. The main alternative, adding a new core progress model, is broader than needed for this channel-local rendering and lifecycle bug.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against d47308f8008c.

Label changes

Label changes:

  • add proof: 📸 screenshot: Contributor real behavior proof includes screenshot evidence. The PR provides real Telegram Desktop screenshot proof for the visible lane rendering and collapse behavior, plus Mantis native Telegram proof on the visual path; the latest head only adds a non-visible guarded fallback regression.
  • add rating: 🦞 diamond lobster: Overall readiness is 🦞 diamond lobster; proof is 🦞 diamond lobster and patch quality is 🦞 diamond lobster.
  • remove rating: 🐚 platinum hermit: Current PR rating is rating: 🦞 diamond lobster, so this older rating label is no longer current.

Label justifications:

  • P2: This is a bounded Telegram channel streaming fix with visible progress-message impact but no core outage, data-loss, or security emergency.
  • rating: 🦞 diamond lobster: Overall readiness is 🦞 diamond lobster; proof is 🦞 diamond lobster and patch quality is 🦞 diamond lobster.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (screenshot): The PR provides real Telegram Desktop screenshot proof for the visible lane rendering and collapse behavior, plus Mantis native Telegram proof on the visual path; the latest head only adds a non-visible guarded fallback regression.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR provides real Telegram Desktop screenshot proof for the visible lane rendering and collapse behavior, plus Mantis native Telegram proof on the visual path; the latest head only adds a non-visible guarded fallback regression.
  • proof: 📸 screenshot: Contributor real behavior proof includes screenshot evidence. The PR provides real Telegram Desktop screenshot proof for the visible lane rendering and collapse behavior, plus Mantis native Telegram proof on the visual path; the latest head only adds a non-visible guarded fallback regression.
  • mantis: telegram-visible-proof: Mantis should capture Telegram visible proof. The PR changes visible Telegram streamed progress message formatting, collapse-bar output, and window positioning that are suitable for native Telegram proof.
Evidence reviewed

PR surface:

Source +664, Tests +1125. Total +1789 across 9 files.

View PR surface stats
Area Files Added Removed Net
Source 5 697 33 +664
Tests 4 1180 55 +1125
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 9 1877 88 +1789

What I checked:

  • Repository policy applied: Read the full root policy plus scoped extension and Telegram policies; Telegram streaming changes require real Telegram proof and plugin-boundary review. (AGENTS.md:1, d47308f8008c)
  • Current main lacks the Telegram lane presentation being repaired: Current main creates the shared progress compositor without Telegram reasoning/commentary lane prefixes, so the PR is not already implemented on main. (extensions/telegram/src/bot-message-dispatch.ts:1020, d47308f8008c)
  • Patch uses the shared compositor contract instead of a parallel implementation: The PR passes Telegram-specific reasoning/commentary prefixes and commentary italic behavior into createChannelProgressDraftCompositor while keeping the core compositor as the source of lane state. (extensions/telegram/src/bot-message-dispatch.ts:1070, 8e5bcf67b063)
  • Patch fixes HTML transport and preview lifecycle in the Telegram layer: The parse_mode=HTML path now normalizes
    to newlines, and finalizeToPreview edits or materializes the live window instead of forcing delete/repost behavior. (extensions/telegram/src/draft-stream.ts:117, 8e5bcf67b063)
  • Regression coverage targets the reviewed behavior: The tests cover lane markdown rendering, long reasoning clipping, multi-line commentary safety, stationary-window collapse, no-live-message fallback, and non-blocking cosmetic bar send failures. (extensions/telegram/src/bot-message-dispatch.test.ts:3330, 8e5bcf67b063)
  • CI is green at the reviewed head: The CI workflow for head 8e5bcf6 completed successfully, including channel contracts, lint, prod types, test types, and QA smoke checks surfaced by gh. (8e5bcf67b063)

Likely related people:

  • obviyus: Authored the shared progress compositor and Telegram progress-draft work that this PR extends, and also appears in recent Telegram progress cleanup history. (role: feature-history owner; confidence: high; commits: 04d221b586bb, 799a0af74fdd, 9782d7f505cb; files: extensions/telegram/src/bot-message-dispatch.ts, src/channels/progress-draft-compositor.ts)
  • anagnorisis2peripeteia: Authored merged commentary bridge and verbose progress work that feeds the Telegram commentary/progress paths reviewed here. (role: adjacent commentary delivery contributor; confidence: medium; commits: 7aa4c8230e64, 4098d57ae0df, b762121d902e; files: src/auto-reply/reply/agent-runner-cli-dispatch.ts, extensions/telegram/src/bot-message-dispatch.ts, extensions/discord/src/monitor/message-handler.process.ts)
  • zhangguiping-xydt: Authored the recent merged Telegram progress-draft cleanup before tool output, which touches the same progress-window delivery behavior. (role: recent adjacent contributor; confidence: medium; commits: 4f12fe28f85c, 4bffede7d539; files: extensions/telegram/src/bot-message-dispatch.ts)
  • Peetiegonzalez: Authored this PR's reviewed commits, including the lane-rendering, collapse-summary, stationary-window, and guarded-fallback changes. (role: current patch author; confidence: high; commits: 812221c394eb, 8e5bcf67b063; files: extensions/telegram/src/bot-message-dispatch.ts, extensions/telegram/src/draft-stream.ts, extensions/telegram/src/progress-summary.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. proof: 📸 screenshot Contributor real behavior proof includes screenshot evidence. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. mantis: telegram-visible-proof Mantis should capture Telegram visible proof. P2 Normal backlog priority with limited blast radius. labels Jul 2, 2026
@Marvinthebored
Marvinthebored force-pushed the fix/telegram-streamed-progress-lanes branch from aaf0dc7 to 2025a5f Compare July 2, 2026 05:43
@Marvinthebored
Marvinthebored force-pushed the fix/telegram-streamed-progress-lanes branch 2 times, most recently from 1406fb6 to bd56764 Compare July 2, 2026 07:24
@clawsweeper clawsweeper Bot added rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. and removed proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. labels Jul 2, 2026
@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. and removed rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. labels Jul 2, 2026
@Marvinthebored
Marvinthebored force-pushed the fix/telegram-streamed-progress-lanes branch 3 times, most recently from ff8e62a to a55e12a Compare July 2, 2026 16:41
Peetiegonzalez and others added 11 commits July 3, 2026 07:09
The streaming "gerund" progress box was deleteMessage'd immediately at
teardown, so on fast turns it flashed and vanished before it could be read,
and the delete could race a just-persisted message (intermittently dropping
the first verbose commentary). Add MIN_PREVIEW_DWELL_MS (4000ms) and schedule
the delete DETACHED via setTimeout for max(0, 4000 - timeVisible), measured
from when the box first appeared. The delete never awaits, so turn teardown
is never stalled.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Port the dev-beta2 progress-summary work onto the compositor-era dispatch:

- Post a one-line activity digest (🧠 N thoughts · 💬 N notes · 🛠️ N tool
  calls · ⏱️ Ns, Discord parity) as a durable message when the progress
  window collapses at end-of-turn, before the final answer. Counted
  per-burst channel-side; only window-streamed activity feeds the bar
  (durable rv/verbose items never do — persistent message XOR bar count),
  no bar for error finals or turns where the window never rendered.
- Render multi-line commentary with its line structure intact (per-line
  markdown conversion, joined as line breaks) instead of collapsing to
  one run-on line; per-line conversion also keeps block markdown (setext
  headings) from forming across lines.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Five progress-window fixes for the Telegram streaming lane, aligning it with
the Discord reference surface:

- FIX 2: /reasoning on + /verbose off no longer kills the progress window.
  forceBlockStreamingForReasoning is now scoped to non-progress modes, so
  durable reasoning removes only the 🧠 lane; commentary/tools still stream and
  the collapse bar still posts.
- FIX 3: durable thoughts render behind the 🧠 marker instead of the literal
  "Thinking" header. The core formatReasoningMessage output is rewritten
  channel-side in reasoning-lane-coordinator (no core change), keeping the
  italic body.
- FIX 4: /verbose on no longer duplicates tool calls. canPushStreamToolProgress
  now yields under verbose so the durable verbose lane owns every progress
  surface (invariant: persistent message XOR window).
- FIX 1: the progress window collapses by EDITING the existing message in place
  into the summary bar (draft-stream finalizeToPreview), then posts the final
  below — no delete + repost, which scroll-jumped the client. Falls back to
  clearing the window when there is no bar to collapse into.
- FIX 5: message_tool_only/codex finals that bypass the in-band answer path now
  post the collapse bar from a cleanup-time fallback (sawProgressFinal from the
  dispatch counts).

Tests: adapted predating dispatch/reasoning tests to the new 🧠 marker and
collapse-by-edit behavior; added coverage for FIX 2 (window alive under
/reasoning on), FIX 4 (no window tool dup under verbose), and FIX 5
(message_tool_only collapse bar). 220 green across bot-message-dispatch,
draft-stream, progress-summary.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Live on-off finding: the collapse path was inconsistent. Edit-in-place worked
in off-off/stream-off, but in on-off (tool-progress-only window, reasoning
durable) finalizeToPreview returned undefined and the caller fell into a bare
clear() -> deleteMessage -> Telegram focus-jump; a sibling sub-branch dropped
the bar entirely. Two divergent outcomes from one fallback.

Root cause: a throttled tool-progress preview could still be pending (coalesced,
never sent) when the turn ended, so the window had no message id even though it
had "rendered". finalizeToPreview gave up and the dispatch fallback deleted the
(late-landing) window.

Fixes:
- draft-stream finalizeToPreview: settle the stream, then MATERIALIZE a still-
  pending preview (send it, as a final flush would) so the window message exists
  and can be edited in place. Only when no message could be established does it
  return undefined. (draft-stream.ts:597)
- dispatch collapse: one deterministic path returning "edited" | "posted" |
  "none". A bar is ALWAYS surfaced when one exists — edited in place, or posted
  durably with ZERO deleteMessage. clear()/delete now runs ONLY for the "none"
  case (error final or nothing to summarize), never when a bar exists, so no
  collapse path can focus-jump. Split into resolveProgressCollapseSummaryLine /
  collapseProgressWindowIntoSummary / resetAnswerLaneAfterCollapse /
  teardownProgressWindow for a readable branch. (bot-message-dispatch.ts:1949)

Tests: added on-off dispatch repro (tool-progress-only + durable reasoning +
mid-turn rotation + final -> edits into bar, no clear, exactly one bar), the
no-live-message durable-bar-without-delete case, and three draft-stream
finalizeToPreview cases (edit-in-place, pending-materialize, no-window).
225 green across bot-message-dispatch, draft-stream, progress-summary;
extensions/telegram typechecks clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…erred

Peter isolated the on-off focus-jump precisely: when a durable 🧠 posts BELOW
the streaming window, the window (now above the reasoning) is repositioned to
stay newest by DELETING it and reposting below. Telegram cannot move messages,
so the delete-then-repost order scroll-jumps the client.

Root cause: rotateAnswerLaneAfterToolProgress rewound the tool-progress window
with stream.clear(), which deletes the old message IMMEDIATELY when it has been
on screen past the dwell — before the replacement message is sent. Delete-first,
post-second.

Fix — invert the order, preserving arrival order:
- draft-stream: new rotateToNewMessageDeferringDelete() rewinds the stream so the
  NEXT update creates a fresh message, and schedules the superseded message's
  delete for AFTER it (detached, floored at 1.5s so the new message lands first).
  Extracted the shared deferred-delete scheduler (scheduleDetachedDelete) used by
  both clear() and the reposition. (draft-stream.ts)
- dispatch: rotateAnswerLaneAfterToolProgress now repositions via that method
  instead of clear()+forceNewMessage, so no window reposition deletes before the
  replacement lands. (bot-message-dispatch.ts:1259)

Tests: draft-stream unit tests prove the sequencing (new message sent before the
old is deleted; delete deferred; no-op with no live message). Added a dispatch
repro (durable 🧠 then answer text mid-turn -> reposition, no clear). Updated the
predating tool-progress-rotation tests to assert the deferred-delete reposition
and that any deliverer-cleanup clear() runs only AFTER the rewind. 228 green
across bot-message-dispatch, draft-stream, progress-summary; extensions/telegram
typechecks clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…nchor loss)

Live off-off finding (no reposition involved): the final answer posted below the
viewport. Cause: collapseProgressWindowIntoSummary edited the tall window bubble
DOWN to a one-line bar BEFORE the final was sent; shrinking content under the
viewport breaks the Telegram client's at-bottom follow, so the freshly sent final
landed off-screen.

Fix — swap the order in deliverProgressModeFinalAnswer:
- Snapshot the bar line (resolveProgressCollapseSummaryLine, which also consumes
  the once-guard) BEFORE the final send, so the final's own delivery cannot
  perturb the counts/timer.
- Send the final answer FIRST (it lands at the bottom of the anchored viewport),
  THEN collapse the window above it. The bar is an edit of an already-posted
  message that sits above the final, so shrinking it is harmless.
- Failure path: if the final send skips/fails, still collapse to the bar (or tear
  the window down when there is nothing to summarize) so no stale window lingers.
- Mark answerLane.finalized / markProgressFinalDelivered LAST, after the collapse
  resets lane state, so end-of-turn cleanup sees a finalized lane (stop(), not a
  spurious clear()).

Renamed collapseProgressWindowIntoSummary -> applyProgressCollapseSummary (takes
the pre-resolved line; "edited" | "posted").

Tests: updated the collapse tests to assert the new order (final delivered before
finalizeToPreview); added "sends the final answer before collapsing the window
into the bar" (order + counter snapshot) and "still collapses the window when the
final answer send is skipped". 230 green across bot-message-dispatch, draft-stream,
progress-summary; extensions/telegram typechecks clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…n churn)

Progress mode now keeps ONE window message per turn, edited through every lane
handover and edited into the bar only at collapse — matching Discord's
single-message model. Previously the tool-progress window was rotated/repositioned
to a fresh bubble on every interim answer chunk and on the tools->text handover,
even though progress mode never renders interim answer text into the window
(updateDraftFromPartial already returns early for the answer lane). Each rotation
spawned a new message; that churn was the on-off jump's underlying source.

Fix — progress-mode-gated so the rotation branches fire only where they must
(block/partial); block/partial and finalized-rotation paths are untouched:
- prepareAnswerLaneForText returns early in progress mode — no rotate/reposition
  for interim answer text that never displays. (bot-message-dispatch.ts:1288)
- prepareAnswerLaneForToolProgress no longer rotates in progress mode; the tool
  lane always edits the same message (rotate stays for block/partial where answer
  text streamed first). (bot-message-dispatch.ts:1064)
- the interim answer-block path no longer rotates the window to a new bubble in
  progress mode. (bot-message-dispatch.ts:2356)

Rotation/delete paths REMAIN (all post-first / deferred-delete per cc9c2b1)
only for: block mode, forceNewMessage after a finalized answer (new turn), error
teardown with nothing to summarize, and superseded-generation cleanup. Collapse
uses task-9 order (final first, then edit the one window into the bar in place);
zero deleteMessage in the happy path. The end-of-turn forceNewMessage-after-
collapse is unchanged (end-of-turn, not mid-turn churn).

LOC: net +~24 prod lines, all justified — three small streamMode==="progress"
guards plus the invariant comments AGENTS.md requires for intentional caller
differences. The rotation branches are shared with block/partial and cannot be
deleted, only gated.

Tests: removed the now-obsolete task-8 "repositions the tool-progress window"
test (that path no longer runs in progress mode); added "keeps a single
stationary window when text follows durable reasoning" and a multi-boundary turn
"uses one stationary window message across a multi-boundary turn
(commentary->tool->commentary->tool->final)" asserting one window message id used
throughout, edited N times, zero deletes, one bar edit last, final posted before
the bar edit. 231 green across bot-message-dispatch, draft-stream, progress-summary;
extensions/telegram typechecks clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Progress mode's streaming window is a pure activity log (Discord parity): it
carries only 🧠/💬/🛠️ progress lines, and the answer appears once, as the final
message below the collapsed bar. Answer PARTIALS were already suppressed
(updateDraftFromPartial early-returns for the answer lane), but intermediate
answer BLOCKS (info.kind === "block", before the final) still streamed into the
window via deliverLaneText, so interim assistant prose flashed inside the
working bubble mid-turn.

Fix: in the deliver() answer-block branch, suppress plain interim answer blocks
in progress mode — buffer them (like the existing skipTextOnlyBlock path) so they
still feed the final/collapse, and skip the draft-stream delivery. Media,
approval, and button blocks are not plain interim prose and fall through to
normal delivery. With plain blocks now suppressed upstream, the block-branch's
progress rotation guard only guards those remaining media/button blocks; comment
updated to say so.

This is the product decision Peter approved (interim answer-block visibility in
the window); it completes the single-message model so the window truly only ever
shows progress lines.

Tests: added "never streams an interim answer block into the progress window
(Discord parity)" — an interim block never reaches update/updatePreview and never
appears in delivered replies, while the final answer is delivered below. 233
green across bot-message-dispatch, draft-stream, progress-summary;
extensions/telegram typechecks clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Addresses five codex red-team findings on the streamed progress-window
collapse bar. Channel-side only; no core src/** changes.

F1 (transcript pollution): the collapse bar went through
sendPayload({durable:true}), which unconditionally passed the transcript
mirror to the deliverReplies fallback, writing the cosmetic digest
("💬 1 note · 🛠️ 1 tool call · ⏱️ 4s") into the session transcript so the
model read it back as its own prior turn. Added a mirrorTranscript option to
sendPayload; the bar now sends durably with mirrorTranscript:false. Discord
parity: its summary bar (monitor/reply-delivery.ts deliverDiscordReply →
sendDurableMessageBatch) has no transcript-mirror seam at all. Real finals
keep mirroring.

F4 (tool overcount): progressSummary.noteToolCall() fired for ANY start-phase
tool, but the compositor renders a line only for work tools
(isChannelProgressDraftWorkToolName) and only when toolProgress is on, so
codex/message_tool_only turns showed "🛠️ 1 tool call" with no tool line. The
count is now gated by the same public work-tool-name check plus
streamToolProgressEnabled; non-counting tool starts still close the
reasoning/commentary bursts as a boundary.

F2 (silent collapse drop): finalizeToPreview ignored a false return from the
in-place edit (flood-wait 429 / terminal error), so applyProgressCollapseSummary
assumed "edited", cleared state, posted no bar, and left the tall window.
finalizeToPreview now returns undefined when the edit did not apply, so the
dispatch falls back to the existing durable-post path.

F3 (cosmetic send fails the turn): the cleanup-time deliverProgressCollapseSummary
could throw (429/network) and propagate out of dispatch after the real final
already delivered. The bar send is now wrapped: failures log via logVerbose and
never fail the turn; the once-guard is preserved.

F5 (ghost-preview race): rotateToNewMessageDeferringDelete rewound while a FIRST
send was still in flight; the late send landed as superseded {retain:true}, which
the dispatch handler kept as an orphaned stale bubble. A reposition now records
the in-flight generation and deletes its late-landing message (deferred), while
forceNewMessage's retain-as-durable-chunk contract is unchanged.

Tests: added unit tests for each invariant (bar delivered but absent from the
transcript mirror; message-tool start → no 🛠️ count and toolProgress-off → no
count; failed edit → undefined; bar send throw → turn still succeeds; reposition
race → superseded send deleted not retained). Telegram suites green
(bot-message-dispatch 165, draft-stream 58, progress-summary 16); extensions
tsgo clean for telegram.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Post-rebase onto main, check-test-types/lint failed on our touched test
files (runtime unaffected):
- core's onVerboseProgressVisibility now takes a thunk (isActive: () =>
  boolean); the dispatch test passed a bare boolean. Pass () => true.
- our TelegramDraftStream additions (finalizeToPreview,
  rotateToNewMessageDeferringDelete) are required members; the QA-e2e
  mock streams omitted them. Add both to every mock stream.
- drop a redundant 'as string' cast (oxlint no-unnecessary-type-assertion).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
ClawSweeper review finding: F3 only wrapped the cleanup-fallback bar send.
The normal path applyProgressCollapseSummary awaited an unguarded durable
send when finalizeToPreview could not edit in place, and sendPayload throws
durable.error on delivery failure — so a cosmetic summary-bar flood-wait
could propagate and fail an otherwise-complete turn (merge-risk:
message-delivery).

Route BOTH cosmetic-bar sends through one shared guarded helper
postCosmeticSummaryBar (swallow + logVerbose), so neither the cleanup path
nor the finalizeToPreview-miss path can fail turn delivery. Add the missing
regression: no-live-message fallback bar send throwing keeps the turn alive
and the final answer delivered.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@Marvinthebored
Marvinthebored force-pushed the fix/telegram-streamed-progress-lanes branch from 8b5300a to 8e5bcf6 Compare July 2, 2026 23:14
@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. proof: 📸 screenshot Contributor real behavior proof includes screenshot evidence. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. and removed proof: 📸 screenshot Contributor real behavior proof includes screenshot evidence. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: 🔁 re-review loop A fresh ClawSweeper review was explicitly requested after the latest review. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. labels Jul 2, 2026
@obviyus
obviyus merged commit dae86e6 into openclaw:main Jul 3, 2026
133 of 148 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: telegram Channel integration: telegram mantis: telegram-visible-proof Mantis should capture Telegram visible proof. P2 Normal backlog priority with limited blast radius. proof: 📸 screenshot Contributor real behavior proof includes screenshot evidence. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. size: XL status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants