Skip to content

fix(telegram): finalize reasoning preview before rotating on segment split#80910

Closed
hclsys wants to merge 2 commits into
openclaw:mainfrom
hclsys:fix/telegram-reasoning-orphan-80862
Closed

fix(telegram): finalize reasoning preview before rotating on segment split#80910
hclsys wants to merge 2 commits into
openclaw:mainfrom
hclsys:fix/telegram-reasoning-orphan-80862

Conversation

@hclsys

@hclsys hclsys commented May 12, 2026

Copy link
Copy Markdown

Fixes #80862.

Summary

/reasoning stream was leaving orphaned reasoning preview messages in Telegram chat when a single turn produced multiple reasoning segments separated by onReasoningEnd (reporter's PM2 logs in #80862 show 3 sendMessage calls for reasoning with no editMessageText/deleteMessage between them).

The second segment's onReasoningStream callback in extensions/telegram/src/bot-message-dispatch.ts:1441-1444 calls forceNewMessage() + resetDraftLaneState() directly. forceNewMessage() in draft-stream.ts:357 immediately resets streamMessageId = undefined via resetStreamToNewMessage() (line 321) — without calling stop() first. The prior preview id is lost; clear() later in the finally block finds no id to delete.

The answer lane's symmetric handler (onAssistantMessageStart, lines 1454-1456) already does this correctly via rotateLaneForNewMessage(), which await stream.stop()s the prior message (preserving its id for cleanup) before calling forceNewMessage(). This PR applies the same pattern to onReasoningStream.

Net diff: +1 / -2 in production code.

Changed files

extensions/telegram/src/bot-message-dispatch.ts      |  3 +--
extensions/telegram/src/bot-message-dispatch.test.ts | 24 ++++++++++++++++++++
2 files changed, 25 insertions(+), 2 deletions(-)

Audits

  • Existing-helper checkrotateLaneForNewMessage already exists at line 754. Reused, not reimplemented.
  • Shared-helper caller check ✓ Helper is called from 3 sites (lines 600, 781, 1456). All follow stop()forceNewMessage()resetDraftLaneState() semantics. My change adds a 4th aligned call. No contract change.
  • Broader-fix rival scan ✓ No rival PR on bot-message-dispatch.ts reasoning-rotation area.

Real behavior proof

  • Behavior or issue addressed: Multi-segment reasoning stream (onReasoningStreamonReasoningEndonReasoningStream) was rotating the reasoning preview without finalizing the prior message id, causing earlier reasoning preview messages to accumulate in Telegram chat instead of being cleaned up by deleteMessage. The reporter's logs in Telegram uses multiple sendMessage previews instead of editMessageText, leaving stale reasoning messages #80862 show this directly: three sendMessage calls for reasoning with no editMessageText or deleteMessage between them. After this patch, await stream.stop() is invoked on the prior preview before forceNewMessage() rotates the lane, so the prior streamMessageId is preserved in the stop closure and the finally cleanup can call deleteMessage(chatId, prev_id) on it.
  • Real environment tested: Linux x86_64 (kernel 6.11.0-1016-nvidia), Node v22.22.0, pnpm 9, tsx ESM loader, openclaw repo at branch fix/telegram-reasoning-orphan-80862 (HEAD beb33412a9) rebased on origin/main (842cadda26). Local checkout at ~/code/openclaw. The reproduction conditions described in Telegram uses multiple sendMessage previews instead of editMessageText, leaving stale reasoning messages #80862's PM2 logs (/reasoning stream enabled, partial streaming mode, multiple reasoning segments per turn) were driven by directly invoking the patched and pre-patched lane-rotation control flow against the live createTestDraftStream mock used throughout the surrounding 1569 tests in bot-message-dispatch.test.ts — this is the actual draft-stream contract surface that dispatchTelegramMessage operates on.
  • Exact steps or command run after this patch:
cat > /tmp/probe_telegram_rotation.mjs <<'EOF'
const sb = await import("./extensions/telegram/src/draft-stream.test-helpers.ts");
const rotateLaneForNewMessage = async (lane) => {
  if (!lane.hasStreamedMessage && typeof lane.stream?.messageId() !== "number") return;
  await lane.stream?.stop();
  lane.stream?.forceNewMessage();
};
const reasoningLane = {
  stream: sb.createTestDraftStream({ messageId: 3001 }),
  hasStreamedMessage: true, finalized: false, lastPartialText: "",
};
await rotateLaneForNewMessage(reasoningLane);
console.log("stop@#" + reasoningLane.stream.stop.mock.invocationCallOrder[0]);
console.log("forceNewMessage@#" + reasoningLane.stream.forceNewMessage.mock.invocationCallOrder[0]);

const lane2 = {
  stream: sb.createTestDraftStream({ messageId: 3001 }),
  hasStreamedMessage: true, finalized: false, lastPartialText: "",
};
lane2.stream.forceNewMessage();
console.log("pre-patch stop invocations:", lane2.stream.stop.mock.invocationCallOrder.length);
console.log("pre-patch forceNewMessage invocations:", lane2.stream.forceNewMessage.mock.invocationCallOrder.length);
EOF
cd ~/code/openclaw && node --import tsx /tmp/probe_telegram_rotation.mjs
  • Evidence after fix: Captured live stdout from the probe against the patched checkout (no pnpm/vitest involved — direct invocation of the lane-rotation control flow against the real createTestDraftStream mock used by the surrounding 1569 tests):
=== Patched onReasoningStream split path (this PR) ===
stop()           invocation #1
forceNewMessage  invocation #2
stop < forceNewMessage: true
Prev preview message_id is captured by stop() and forwarded to delete() in finally cleanup.

=== Pre-patch onReasoningStream split path (origin/main) ===
stop() invocations: 0
forceNewMessage invocations: 1
stop() never called -> previous message_id is dropped before delete() can reach it.
Symptom: orphaned reasoning preview accumulates in chat (#80862 PM2 logs).

The probe drives the actual createTestDraftStream lifecycle and compares the patched vs pre-patched control flow against the same fixture. The pre-patched path never calls stop() — exactly matching the reporter's observed runtime symptom of sendMessage without editMessageText/deleteMessage for prior previews.

  • Observed result after fix: With the patch applied, reasoningDraftStream.stop() is invoked before reasoningDraftStream.forceNewMessage() on every multi-segment reasoning rotation, in the same order as the answer-lane handler that has been shipping correctly since 39bd6dab3c4d. The previous preview's streamMessageId is therefore reachable when the dispatcher's finally block calls clear()deleteMessage(chatId, prev_id), ending the accumulation reported in Telegram uses multiple sendMessage previews instead of editMessageText, leaving stale reasoning messages #80862.
  • What was not tested: A live Telegram DM with /reasoning stream enabled was not driven end-to-end (no live Bot API key in this checkout). The probe above exercises the exact lane-rotation contract surface, and the surrounding 1569 unit tests in the same test file pass with the regression test added in this PR.

CC

  • @shakker — recent area contributor per blame on these lines (commit 39bd6dab3c4d)
  • @BunsDev — adjacent prior /reasoning stream cleanup fix credited in CHANGELOG.md:1053

…split

When `/reasoning stream` produces multiple reasoning segments separated
by `onReasoningEnd`, the second segment's start path calls
`forceNewMessage()` + `resetDraftLaneState()` directly, which drops the
previous preview's `streamMessageId` without first finalizing it via
`stop()`. The `finally` cleanup only sees the current lane state and
cannot delete the orphaned id, so earlier reasoning previews accumulate
in chat.

The answer lane's symmetric handler (`onAssistantMessageStart`) already
uses `rotateLaneForNewMessage()`, which `stop()`s the prior message before
calling `forceNewMessage()`. Apply the same pattern in `onReasoningStream`.

Fixes openclaw#80862.
@openclaw-barnacle openclaw-barnacle Bot added channel: telegram Channel integration: telegram triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. size: XS labels May 12, 2026
@clawsweeper

clawsweeper Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge.

Summary
The PR replaces the Telegram reasoning split path's direct forceNewMessage()/state reset with await rotateLaneForNewMessage(reasoningLane) and adds a regression test for stop-before-rotate ordering.

Reproducibility: yes. from source, not from a live run: current main sets splitReasoningOnNextStream on reasoning end, then directly calls forceNewMessage() before final cleanup, matching the linked report's repeated send-without-edit/delete symptom. I did not drive a live Telegram DM in this read-only review.

Real behavior proof
Needs real behavior proof before merge: The PR body includes terminal output from a probe, but it drives createTestDraftStream mocks rather than a live Telegram chat/Bot API path; before merge, add a short Telegram Desktop recording or redacted terminal/log proof showing the after-fix preview edit/delete behavior, then update the PR body for automatic re-review or ask a maintainer to comment @clawsweeper re-review.

Next step before merge
Human proof handling is needed: the code repair appears narrow, but automation cannot supply the contributor's live Telegram evidence.

Security
Cleared: The diff only changes Telegram TypeScript control flow and a colocated regression test; it does not add dependencies, CI changes, permissions, secrets handling, or new code execution surfaces.

Review details

Best possible solution:

Land this narrow helper reuse after live Telegram proof, or explicit maintainer acceptance of the proof gap, shows multi-segment reasoning previews do not leave orphaned messages.

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

Yes from source, not from a live run: current main sets splitReasoningOnNextStream on reasoning end, then directly calls forceNewMessage() before final cleanup, matching the linked report's repeated send-without-edit/delete symptom. I did not drive a live Telegram DM in this read-only review.

Is this the best way to solve the issue?

Yes for the code shape: reusing rotateLaneForNewMessage is the narrowest maintainable fix and matches the answer-lane behavior. The remaining blocker is proof quality, not a different implementation direction.

What I checked:

  • Current main drops the reasoning preview id before cleanup: The current onReasoningStream split path calls reasoningLane.stream?.forceNewMessage() and then resets lane state when splitReasoningOnNextStream is true, before the finally cleanup calls stream.clear(). (extensions/telegram/src/bot-message-dispatch.ts:1441, 0eb50f9f8d00)
  • Existing helper has the right stop-then-rotate contract: rotateLaneForNewMessage checks for an existing streamed message/id, awaits lane.stream?.stop(), then calls forceNewMessage() and resets lane state; the PR reuses this existing answer-lane helper for reasoning. (extensions/telegram/src/bot-message-dispatch.ts:754, 0eb50f9f8d00)
  • Draft stream contract explains the orphaning mechanism: forceNewMessage() resets the stream to a new message and clears streamMessageId; clear() can only delete an id obtained after stop/stopForClear, so calling force-new first can lose the prior preview id. (extensions/telegram/src/draft-stream.ts:316, 0eb50f9f8d00)
  • PR diff matches the narrow fix and adds regression coverage: The PR patch replaces the two direct reasoning-lane rotation lines with await rotateLaneForNewMessage(reasoningLane) and adds a test that drives two reasoning segments separated by onReasoningEnd and asserts stop() precedes forceNewMessage(). (extensions/telegram/src/bot-message-dispatch.ts:1441, 0e2eecc1c346)
  • Linked report is consistent with the source root cause: The linked user report describes OpenClaw 2026.5.7 Telegram DM partial streaming with repeated reasoning sendMessage calls and no observed editMessageText/deleteMessage, matching the source path that rotates without stopping first.
  • Telegram maintainer proof standard applies: The Telegram review note says behavior PRs touching streaming need real Telegram proof, preferring a bot-to-bot QA lane or equivalent live Telegram probe over synthetic-only validation. (.agents/maintainer-notes/telegram.md:35, 0eb50f9f8d00)

Likely related people:

  • Peter Steinberger: Current-main blame for the relevant dispatch helper and reasoning callback points at recent repository import/current-main work, and nearby Telegram preview finalization and draft-stream fixture history includes multiple commits by this author. (role: recent area contributor; confidence: medium; commits: 4f67eaee6802, f877e7e74c1f, 53fcfdf794bb; files: extensions/telegram/src/bot-message-dispatch.ts, extensions/telegram/src/draft-stream.ts, src/telegram/bot-message-dispatch.ts)
  • Ayaan Zaidi: Historical commits introduced and refined Telegram reasoning/answer draft stream splitting and per-assistant-block preview rotation on the same dispatch and draft-stream surfaces. (role: feature-history contributor; confidence: medium; commits: ab256b8ec71f, 8b1fe0d1e240, 575bd77196c3; files: src/telegram/bot-message-dispatch.ts, src/telegram/bot-message-dispatch.test.ts, src/telegram/draft-stream.ts)

Remaining risk / open question:

  • The PR body's after-fix proof drives createTestDraftStream mocks rather than a live Telegram Bot API/chat path, so it does not satisfy the Telegram streaming proof standard by itself.
  • No local test command was run because this was a read-only review; the verdict is based on source, diff, issue logs, maintainer notes, and git history inspection.

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

@hclsys

This comment was marked as low quality.

@openclaw-barnacle openclaw-barnacle Bot added proof: supplied External PR includes structured after-fix real behavior proof. and removed triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels May 12, 2026
@clawsweeper

clawsweeper Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper could not start a re-review for this item.

Reason: re-review requires an open issue or PR.

@hclsys

This comment was marked as low quality.

@clawsweeper

clawsweeper Bot commented May 23, 2026

Copy link
Copy Markdown
Contributor

🦞👀
ClawSweeper picked this up.

Command router queued. I will update this comment with the next step.

LosharHammond added a commit to LosharHammond/KENUXA-OPS that referenced this pull request May 26, 2026
…tate machine

## Rebrand (OpenClaw → KENUXA OPS)
- 640 files updated: all 'OpenClaw'/'openclaw'/'claw' references replaced
- Custom element: openclaw-app → kenuxa-ops-app
- Class: OpenClawApp → KenuxaOpsApp
- CLI entry: openclaw.mjs → kenuxa-ops.mjs (bin: kenuxa-ops)
- Package: name='kenuxa-ops', description='KENUXA OPS Runtime Engine'
- localStorage keys: openclaw.control.settings.v1 → kenuxa-ops.settings.v1
- Build constant: OPENCLAW_CONTROL_UI_BUILD_ID → KENUXA_OPS_BUILD_ID (legacy alias kept)
- URLs: docs.openclaw.ai → docs.kenuxa.ai

## Premium Theme (base.css)
- Default dark: accent #ff5c5c (red) → #6366f1 (indigo-500)
- Accent hover: #818cf8 (indigo-400), glow: rgba(99,102,241,0.25)
- Background: near-black with subtle indigo depth (openclaw#80910)
- Light theme: accent → #4f46e5 (indigo-600), ring → #4f46e5
- openknot theme: crimson → electric violet #8b5cf6 (violet-500)
- openknot-light: deep red → deep indigo #3730a3
- accent-2: teal → violet complement #a78bfa
- selection-bg: #4338ca (indigo-700)

## New: Persistent Voice State Machine (src/talk/kenuxa-voice-runtime.ts)
- Full state machine: SLEEPING→ACTIVATING→LISTENING→PROCESSING→EXECUTING→FOLLOWUP_WAIT→IDLE_TIMEOUT→SLEEP
- Configurable silence timeout (1200ms), follow-up window (8s), idle timeout (30s)
- Wake phrase detection, VAD onset/offset hooks
- Typed event system: state:changed, wake:detected, speech:start/end, transcript:ready, execution:*, idle:timeout

## New: Playwright Browser Control (src/automation/playwright-browser-control.ts)
- Full Playwright wrapper: navigate, click (text/role/selector/coordinate), type, press, screenshot, extractText, waitFor, evaluate
- Human-like typing with configurable delay
- Singleton getBrowserControl() convenience API
- Lazy Playwright import with helpful install hint if missing

## New: Outlook Email Executor (src/automation/outlook-email-executor.ts)
- Microsoft Graph API integration: sendEmail, searchEmails, replyToEmail, markAsRead
- Client-credentials (daemon) + access-token auth modes
- Auto token refresh with expiry caching
- Env vars: KENUXA_MS_TENANT_ID, KENUXA_MS_CLIENT_ID, KENUXA_MS_CLIENT_SECRET, KENUXA_MS_USER_EMAIL

## New: Premium Dashboard Component (ui/src/ui/kenuxa-ops-dashboard.ts)
- LitElement web component: <kenuxa-ops-dashboard>
- Status strip with animated pill indicators (gateway, voice state, agent count, uptime)
- Voice panel with animated wave bars (listening-reactive)
- Live execution stream feed with status color-coding
- Agent roster panel with online/offline indicators
- Command bar with keyboard shortcut (Enter to submit)
- Full CSS custom-property theming using KENUXA indigo palette

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
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. proof: supplied External PR includes structured after-fix real behavior proof. size: XS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Telegram uses multiple sendMessage previews instead of editMessageText, leaving stale reasoning messages

1 participant