Skip to content

fix(telegram): flush buffered final answer when reasoning delivery is skipped [AI-assisted]#1342

Open
BingqingLyu wants to merge 2 commits into
mainfrom
fork-pr-53762-fix-telegram-reasoning-answer-drop
Open

fix(telegram): flush buffered final answer when reasoning delivery is skipped [AI-assisted]#1342
BingqingLyu wants to merge 2 commits into
mainfrom
fork-pr-53762-fix-telegram-reasoning-answer-drop

Conversation

@BingqingLyu

@BingqingLyu BingqingLyu commented Apr 27, 2026

Copy link
Copy Markdown
Owner

Summary

  • Problem: When streaming: "partial" is enabled for Telegram and the model produces a turn with both a thinking block and a text block after a tool call, OpenClaw silently drops the text block (fixes [Bug]: streaming: partial drops text block when assistant turn contains [thinking, text] openclaw/openclaw#53384).
  • Why it matters: Users receive the reasoning preview but never get the actual answer — a complete silent message loss.
  • What changed: Added a flushBufferedFinalAnswer() call before the early return in the segment delivery loop (bot-message-dispatch.ts) when info.kind === "final". A matching regression test was added.
  • What did NOT change: No behaviour change for non-final deliveries, non-reasoning payloads, or other channels.

Change Type (select all)

  • Bug fix

Scope (select all touched areas)

  • Integrations

Linked Issue/PR

Root Cause / Regression History (if applicable)

  • Root cause: The segment delivery loop in deliverPayload (bot-message-dispatch.ts) processes reasoning and answer segments from a final payload. When a prior turn set activePreviewLifecycleByLane.reasoning = "complete" (because the answer segment processing at lines 678–683 saw reasoningLane.hasStreamedMessage = true), the reasoning delivery in the next turn returns "skipped" (lifecycle is "complete" → falls to sendPayload which fails). When reasoning is skipped, noteReasoningDelivered() is never called, so reasoningStatus stays "hinted". The answer segment then hits shouldBufferFinalAnswer() === true and is buffered. The loop exits via the if (segments.length > 0) { return; } early-return without flushing the buffer.
  • Missing detection / guardrail: The flushBufferedFinalAnswer() call was missing from this early-return path. It existed in the suppressedReasoningOnly path directly below (line 696) but not in the segment-processed path.
  • Prior context: The suppressedReasoningOnly branch already had the flush, making the omission in the segment branch the gap.
  • Why this regressed now: The buffering logic was added to handle the race between reasoning delivery and the final answer, but the "reasoning skipped" edge case after a tool boundary was not covered.

Regression Test Plan (if applicable)

  • Coverage level:
    • Unit test
  • Target test: extensions/telegram/src/bot-message-dispatch.test.ts — "delivers final answer text when reasoning delivery is skipped in combined think-tag payload after tool boundary"
  • Scenario: Two-turn sequence: Turn 1 fires onReasoningStream then delivers an answer-only final (marking reasoning lifecycle "complete"). After onAssistantMessageStart, Turn 2 delivers <think>…</think>answer. The reasoning sendPayload is mocked to fail ({ delivered: false }), leaving the answer buffered. Without the fix the test fails (answer never delivered); with the fix it passes.
  • Why smallest reliable guardrail: Directly exercises the exact code path at the exact failure point with minimal setup.

User-visible / Behavior Changes

After a tool call in a Telegram conversation with streaming: partial and a reasoning-capable model, the assistant's text reply will now be delivered instead of silently dropped.

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • OS: Linux
  • Runtime/container: Node 22 / Bun
  • Model/provider: Any reasoning model (e.g. claude-sonnet-4-6 with extended thinking)
  • Integration/channel: Telegram with streaming: "partial"
  • Relevant config: channels.telegram.streaming: "partial", reasoningLevel: "stream"

Steps

  1. Configure Telegram channel with streaming: "partial" and a reasoning-capable model.
  2. Send a message that triggers a tool call followed by a reply that includes both thinking and a text block.
  3. Observe the Telegram conversation.

Expected

  • Reasoning block is shown as a preview (or skipped if already finalized).
  • Final text answer is delivered as a Telegram message.

Actual (before fix)

  • Reasoning block shown.
  • Text answer silently dropped — user receives no response.

Evidence

  • Failing test/log before + passing after

Test fails before fix:

Number of calls: 1
❯ extensions/telegram/src/bot-message-dispatch.test.ts:2493:33
    expect(editMessageTelegram).toHaveBeenCalledWith(123, 999, "final answer text", ...)
    AssertionError: expected "editMessageTelegram" to have been called with arguments: ...

Test passes after fix: Tests 1 passed (73)

Human Verification (required)

  • Verified scenarios: Two-turn reasoning+answer payload after tool boundary with skipped reasoning delivery; all 73 existing bot-message-dispatch tests still pass; pnpm check clean.
  • Edge cases checked: Single-turn reasoning (unaffected — goes through noteReasoningDelivered path, not the skipped path). suppressedReasoningOnly path (already had flush, unchanged).
  • What I did not verify: Live end-to-end on a real Telegram bot with a reasoning model.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

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

Failure Recovery (if this breaks)

  • Revert commit fix(telegram): flush buffered final answer when reasoning delivery is skipped.
  • Known bad symptoms: if the flush itself throws, the final delivery would surface an error instead of silently dropping — this is strictly better than the previous silent drop.

Risks and Mitigations

  • Risk: flushBufferedFinalAnswer() is now called even when the buffer is empty (it returns early if takeBufferedFinalAnswer() returns undefined), so there is no functional risk for the normal (non-bug) path.
    • Mitigation: flushBufferedFinalAnswer is a no-op when no answer is buffered.

AI-Assisted PR Checklist

  • Built with Claude Code (claude-sonnet-4-6)
  • Fully tested — pnpm test:extension telegram (1020 passed), pnpm check, pnpm build all green
  • I understand what the code does — root cause traced through reasoningStepState, activePreviewLifecycleByLane, flushBufferedFinalAnswer, and deliverLaneText call chain
  • Greptile summary reviewed — informational only, no action items

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: streaming: partial drops text block when assistant turn contains [thinking, text]

2 participants