Skip to content

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

Closed
amitgaur wants to merge 2 commits into
openclaw:mainfrom
amitgaur:fix/telegram-reasoning-answer-drop
Closed

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

Conversation

@amitgaur

@amitgaur amitgaur commented Mar 24, 2026

Copy link
Copy Markdown

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] #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

@openclaw-barnacle openclaw-barnacle Bot added channel: telegram Channel integration: telegram size: S labels Mar 24, 2026
@greptile-apps

greptile-apps Bot commented Mar 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a silent message-loss bug in the Telegram integration where, after a tool call in a streaming: "partial" session with a reasoning-capable model, the assistant's text reply was silently dropped when the reasoning delivery was "skipped" (lifecycle already "complete" from the prior turn).

Root cause: The segment delivery loop in deliverPayload would buffer the answer segment whenever shouldBufferFinalAnswer() was true (reasoning status still "hinted" because noteReasoningDelivered() was never called for a skipped reasoning segment), then exit via the if (segments.length > 0) { return; } early-return without flushing that buffer.

Fix: A single await flushBufferedFinalAnswer() call, guarded by info.kind === "final", is inserted immediately before the early return. This matches the identical pattern already present in the suppressedReasoningOnly path and the canSendAsIs/sendPayload paths directly below. flushBufferedFinalAnswer is safely idempotent (no-op when the buffer is empty), so there is zero risk of double-delivery on the normal path.

Test: A focused regression test reproduces the exact two-turn tool-boundary scenario — Turn 1 finalises reasoning lifecycle to "complete", Turn 2 sends a combined <think>…</think>answer payload, reasoning delivery is mocked to fail, and the test asserts the buffered final answer text is delivered. All 73 existing dispatch tests continue to pass.

Key changes:

  • bot-message-dispatch.ts — adds flushBufferedFinalAnswer() before early-return in the segment branch (lines 692–694)
  • bot-message-dispatch.test.ts — adds regression test covering the "reasoning skipped after tool boundary" path

Confidence Score: 5/5

  • Safe to merge — minimal one-line fix with a no-op fallback, mirroring existing patterns already proven correct in sibling branches.
  • The fix is a single targeted call that is already used identically in every other exit-path of the same function. flushBufferedFinalAnswer returns early when the buffer is empty, so there is no risk of double-delivery or side-effects on non-bug paths. The regression test faithfully reproduces the exact failure mode described in the root cause. No interface changes, no new permissions, no config changes, and all 73 existing tests pass.
  • No files require special attention.

Reviews (1): Last reviewed commit: "fix(telegram): flush buffered final answ..." | Re-trigger Greptile

@amitgaur

Copy link
Copy Markdown
Author

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@amitgaur amitgaur changed the title fix(telegram): flush buffered final answer when reasoning delivery is skipped fix(telegram): flush buffered final answer when reasoning delivery is skipped [AI-assisted] Mar 24, 2026
@amitgaur amitgaur force-pushed the fix/telegram-reasoning-answer-drop branch from 7ea0c67 to b4bc5b7 Compare March 24, 2026 16:19
@amitgaur amitgaur force-pushed the fix/telegram-reasoning-answer-drop branch from b4bc5b7 to 38d385d Compare April 11, 2026 03:48

@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: 38d385d658

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +692 to +694
if (info.kind === "final") {
await flushBufferedFinalAnswer();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Emit message-sent hook when flushing buffered final answers

The new final-segment early-return branch calls flushBufferedFinalAnswer() and then exits, but this flush path drops the deliverLaneText result and never runs emitPreviewFinalizedHook(...) when the buffered answer is finalized via preview edit. In streaming: "partial" flows with sessionKeyForInternalHooks, the user gets the Telegram answer, but the internal message:sent hook is skipped for that turn, unlike the normal segment-delivery path.

Useful? React with 👍 / 👎.

@amitgaur amitgaur force-pushed the fix/telegram-reasoning-answer-drop branch from 38d385d to b25e563 Compare April 11, 2026 04:00

Copy link
Copy Markdown
Author

Rebased onto the latest main, resolved the changelog conflict, and reran the targeted Telegram regression test locally:

pnpm test -- extensions/telegram/src/bot-message-dispatch.test.ts

Result: 1 test file passed, 74 tests passed.

This PR is now mergeable and ready for maintainer review. Relevant reviewers for this area appear to be @vincentkoc and @obviyus.

@openclaw-barnacle

Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added the stale Marked as stale due to inactivity label Apr 29, 2026
@clawsweeper

clawsweeper Bot commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Thanks for the context here. I swept through the related work, and this is now duplicate or superseded.

This PR has the right visible Telegram fix direction, but it is superseded by the open hook-preserving replacement at #83670, which carries the same final-answer flush plus the missing hook/transcript preservation.

Canonical path: Close this branch and review or land the hook-preserving replacement PR at #83670, or an equivalent patch with the same flush, hook, transcript, and regression coverage.

So I’m closing this here and keeping the remaining discussion on #83670.

Review details

Best possible solution:

Close this branch and review or land the hook-preserving replacement PR at #83670, or an equivalent patch with the same flush, hook, transcript, and regression coverage.

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

Yes, from source inspection. Current main can buffer a final answer after a hinted/skipped reasoning segment and then return from the segmented-final branch without draining that buffer.

Is this the best way to solve the issue?

No, this branch is not the best merge target anymore. The missing flush is the right narrow fix, but the active replacement preserves the normal preview-finalized hook and transcript behavior too.

Security review:

Security review cleared: The diff only changes Telegram dispatch control flow, a colocated test, and changelog text; it adds no dependency, workflow, permission, secret, or supply-chain surface.

What I checked:

Likely related people:

  • obviyus: Merged Telegram partial-stream preview and reasoning/answer lane split work that defines the dispatch path touched by this PR. (role: Telegram streaming lane feature owner; confidence: high; commits: 583844ecf6c1, ab256b8ec71f; files: extensions/telegram/src/bot-message-dispatch.ts, extensions/telegram/src/reasoning-lane-coordinator.ts, extensions/telegram/src/bot-message-dispatch.test.ts)
  • vincentkoc: Authored the merged Telegram direct-delivery message:sent hook parity work, and the unresolved finding is about preserving that hook path during buffered final flushes. (role: hook contract contributor; confidence: high; commits: 817fa5462b9f, 017b38954991, 7b88249c9e03; files: extensions/telegram/src/bot-message-dispatch.ts, extensions/telegram/src/bot/delivery.replies.ts)
  • Ayaan Zaidi: Current checkout blame on the dispatcher and reasoning coordinator points the reviewed code snapshot through Ayaan Zaidi's recent Telegram commit, though the useful ownership signal overlaps with @obviyus history. (role: recent current-path carrier; confidence: medium; commits: 3fb5b4bec94d; files: extensions/telegram/src/bot-message-dispatch.ts, extensions/telegram/src/reasoning-lane-coordinator.ts, extensions/telegram/src/bot-message-dispatch.test.ts)

Codex review notes: model gpt-5.5, reasoning high; reviewed against 1c3ff34d752f.

@clawsweeper

clawsweeper Bot commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Codex review: found issues before merge.

What this changes:

This PR adds a Telegram dispatcher flush for buffered final-answer text before the segmented final-delivery early return, adds a regression test for skipped reasoning after a tool boundary, and adds a changelog fix entry.

Maintainer follow-up before merge:

This is already an open implementation PR for a narrow, real Telegram message-loss bug, but it has a specific unresolved hook-preservation review finding and likely needs maintainer or author follow-up on the branch rather than a separate automated replacement branch.

Review findings:

  • [P2] Emit the hook when flushing buffered final answers — extensions/telegram/src/bot-message-dispatch.ts:703
Review details

Best possible solution:

Land this PR or an equivalent Telegram-owned patch after flushBufferedFinalAnswer() returns or emits the LaneDeliveryResult through the same preview-finalized hook path as normal final segment delivery, while preserving current-main compaction-boundary handling and keeping the change scoped to Telegram dispatch, focused tests, and the user-facing changelog entry.

Full review comments:

  • [P2] Emit the hook when flushing buffered final answers — extensions/telegram/src/bot-message-dispatch.ts:703
    The new segmented-final early return calls flushBufferedFinalAnswer() and exits, but that helper discards the deliverLaneText() result. When the buffered answer finalizes an existing preview, Telegram shows the answer but emitPreviewFinalizedHook(...) is never run, so sessionKeyForInternalHooks flows miss the internal message:sent hook unlike normal final segment delivery. Capture the result and route preview-finalized deliveries through the hook before resetting state.
    Confidence: 0.88

Overall correctness: patch is incorrect
Overall confidence: 0.88

Acceptance criteria:

  • pnpm test extensions/telegram/src/bot-message-dispatch.test.ts
  • pnpm test extensions/telegram

What I checked:

Likely related people:

Remaining risk / open question:

  • The PR branch is based on older main while current main has added pendingCompactionReplayBoundary handling in the same segmented-final return path; the final patch should preserve both behaviors during rebase or merge.
  • The added regression checks user-visible answer delivery, but the unresolved hook path needs explicit coverage with sessionKeyForInternalHooks so preview-finalized buffered answers still emit message:sent.

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

@openclaw-barnacle openclaw-barnacle Bot removed the stale Marked as stale due to inactivity label Apr 30, 2026
@clawsweeper clawsweeper Bot added the mantis: telegram-visible-proof Mantis should capture Telegram visible proof. label May 11, 2026
@openclaw-barnacle openclaw-barnacle Bot added the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label May 11, 2026
@bpconnor3-r2

Copy link
Copy Markdown

Adding downstream behavior evidence from an R2/OpenClaw production-style droplet because this PR is the canonical fix path and #83616 was closed as a duplicate of it.

Environment:

  • OpenClaw npm global install: 2026.5.12
  • Runtime: systemd user gateway on Linux, Telegram channel enabled, direct-message visible replies set to automatic
  • Patch applied live to the installed bundle with the same guard shape as this PR: when segmented delivery handles a final payload, call flushBufferedFinalAnswer() before the segments.length > 0 early return

Observed failure mode before the patch:

  • Final output split into reasoning/answer lanes could leave the visible answer buffered after the reasoning segment was handled/skipped.
  • The segmented-delivery branch returned before draining the buffered final answer, so the model completed but Telegram auto-delivery did not send the visible answer.

After-fix proof:

  • Ran a controlled Telegram ingress through the live R2 gateway into an allowlisted real direct Telegram chat.
  • Prompt asked for the exact final text R2_AUTO_OK_SIM.
  • The model final was R2_AUTO_OK_SIM and no message.action send tool was used.
  • The Telegram sent-message cache recorded the outbound Bot API delivery at 2026-05-18T12:45:28Z with message id 27371.
  • A follow-up healthcheck showed the gateway live and the hotfix present: curl /health returned {"ok":true,"status":"live"}, systemd user service was running, and the hotfix checker reported OpenClaw Telegram auto-delivery hotfix present.

Additional validation I ran while preparing #83616:

  • Equivalent upstream patch against current main passed the targeted dispatcher suite:
    OPENCLAW_VITEST_INCLUDE_FILE=/tmp/openclaw-telegram-include.json node scripts/run-vitest.mjs run --config test/vitest/vitest.extension-telegram.config.ts
  • Result: extensions/telegram/src/bot-message-dispatch.test.ts passed, 67 tests.
  • Downstream durable guard merged in bpconnor3-r2/R2-openclaw#254, including an idempotent installed-bundle checker and healthcheck integration so future OpenClaw upgrades flag this regression if it reappears.

This is not a screenshot/recording, but it is after-fix evidence from a live Telegram outbound path, with private chat details redacted.

@bpconnor3-r2

Copy link
Copy Markdown

I opened a hook-preserving replacement branch/PR here: #83670

Why: this keeps the same buffered-final-answer fix direction, but addresses the unresolved review finding on this PR. The replacement flushBufferedFinalAnswer() captures the LaneDeliveryResult from deliverFinalAnswerText() and routes preview-finalized deliveries through the same emitPreviewFinalizedHook(...) path as ordinary final segment delivery, so internal message:sent hooks and transcript mirroring stay consistent.

Validation on the replacement branch:

  • git diff --check
  • OPENCLAW_VITEST_INCLUDE_FILE=/tmp/openclaw-telegram-include-v2.json node scripts/run-vitest.mjs run --config test/vitest/vitest.extension-telegram.config.ts
  • Result: extensions/telegram/src/bot-message-dispatch.test.ts passed, 68 tests

The R2 live Telegram proof remains here: #53762 (comment)

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. labels May 19, 2026
@clawsweeper

clawsweeper Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper applied the proposed close for this PR.

@clawsweeper clawsweeper Bot closed this May 19, 2026
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. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: S status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup.

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