openclaw - ✅(Solved) Fix Telegram status reaction ends on error emoji (😱) despite successful message delivery — Codex OAuth [1 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#69809Fetched 2026-04-22 07:48:00
View on GitHub
Comments
0
Participants
1
Timeline
0
Reactions
0
Author
Participants

Error Message

{ messages: { statusReactions: { enabled: true, emojis: { error: "👍" } } } }

Root Cause

Right now those four flags aren't logged together anywhere, so even at debug level the root cause isn't directly visible. With this line, one debug-run on a trivial prompt decisively pins down which hypothesis holds.

Fix Action

Fix / Workaround

20:20:32.140 [agent/embedded] embedded run start: runId=10906df2-... provider=openai-codex model=gpt-5.4 thinking=medium messageChannel=telegram
20:20:33.148 [agent/embedded] [context-diag] pre-prompt: messages=82 promptChars=318
20:20:39.231 [diagnostic] session state: prev=processing new=idle reason="run_completed" queueDepth=0
20:20:39.520 [diagnostic] lane task done: lane=session:agent:main:telegram:direct:7169013046 durationMs=9261 active=0 queued=0
20:20:40.197 [gateway/channels/telegram] telegram sendMessage ok chat=7169013046 message=4410
20:20:40.199 telegram ingress: chatId=7169013046 dispatchCompleteMs=12292 buffer=inbound-debounce
  • Run completed cleanly: reason="run_completed", active=0 queued=0, durationMs=9261 — not aborted, not timed out
  • Message delivered successfully: telegram sendMessage ok on message=4410
  • No error log lines during this turn (no dispatch failed, no throw, no timeout)
  • Yet the user observed 😱 as the final reaction, which can only come from setError() — meaning !hasFinalResponse was truthy at the finalize check in bot-BwMz6R6-.js:5086
const deliverySummary = deliveryState.snapshot();
if (dispatchError || !deliverySummary.delivered && (...skipped/failed...)) {
  // Send fallback "Something went wrong" text
  sentFallback = (await deliverReplies({
    replies: [{ text: fallbackText }],
    silent: silentErrorReplies && (dispatchError != null || hadErrorReplyFailureOrSkip),
    ...
  })).delivered;
}
const hasFinalResponse = queuedFinal || sentFallback;

PR fix notes

PR #70130: fix(telegram): add diagnostic logging for status reaction error emoji issue

Description (problem / solution / changelog)

Summary

  • Problem: Telegram status reaction ends on 😱 (error) instead of 👍 (done) despite successful message delivery
  • Why it matters: User-visible cosmetic inconsistency; delivery works correctly
  • What changed: Added logVerbose at reaction finalize showing all four relevant flags: queuedFinal, sentFallback, dispatchError, deliverySummary, hasFinalResponse
  • What did NOT change: No behavior change - this is diagnostic only

Change Type

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #69809

Root Cause

  • Root cause: Unknown - three hypotheses proposed:
    • Hypothesis A: queuedFinal never flips to true on streamed/block deliveries
    • Hypothesis B: silent fallback fires unnecessarily when dispatchError is truthy
    • Hypothesis C: hadErrorReplyFailureOrSkip triggered by non-critical payload flag

User-visible / Behavior Changes

  • No user-visible behavior change - diagnostic logging only
  • After fix applied, run --log-level debug to see which flag causes the issue

Repro + Verification

Steps

  1. Apply this PR
  2. Run openclaw gateway --log-level debug
  3. Send a simple prompt to the Telegram bot
  4. Look for log line: telegram: reaction finalize: queuedFinal=...

Expected

  • See diagnostic log showing values of queuedFinal, sentFallback, dispatchError, deliverySummary

Actual

  • Diagnostic line not yet visible until this patch is applied

Human Verification

  • Verified scenarios: Code review - logging only, no behavior change
  • Edge cases checked: None - pure diagnostic
  • What you did not verify: Runtime test with debug logging

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

Changed files

  • extensions/telegram/src/bot-message-dispatch.ts (modified, +5/-0)

Code Example

20:20:32.140 [agent/embedded] embedded run start: runId=10906df2-... provider=openai-codex model=gpt-5.4 thinking=medium messageChannel=telegram
20:20:33.148 [agent/embedded] [context-diag] pre-prompt: messages=82 promptChars=318
20:20:39.231 [diagnostic] session state: prev=processing new=idle reason="run_completed" queueDepth=0
20:20:39.520 [diagnostic] lane task done: lane=session:agent:main:telegram:direct:7169013046 durationMs=9261 active=0 queued=0
20:20:40.197 [gateway/channels/telegram] telegram sendMessage ok chat=7169013046 message=4410
20:20:40.199 telegram ingress: chatId=7169013046 dispatchCompleteMs=12292 buffer=inbound-debounce

---

const deliverySummary = deliveryState.snapshot();
if (dispatchError || !deliverySummary.delivered && (...skipped/failed...)) {
  // Send fallback "Something went wrong" text
  sentFallback = (await deliverReplies({
    replies: [{ text: fallbackText }],
    silent: silentErrorReplies && (dispatchError != null || hadErrorReplyFailureOrSkip),
    ...
  })).delivered;
}
const hasFinalResponse = queuedFinal || sentFallback;

if (statusReactionController && !hasFinalResponse)
  Promise.resolve(statusReactionController.setError())...  // ← sets 😱

if (!hasFinalResponse) {
  clearGroupHistory();
  return;
}

// ... auto-topic-label code ...

if (statusReactionController)
  Promise.resolve(statusReactionController.setDone())...  // ← sets 👍

---

logVerbose(`telegram: reaction finalize: queuedFinal=${queuedFinal} sentFallback=${sentFallback} dispatchError=${Boolean(dispatchError)} deliverySummary=${JSON.stringify(deliverySummary)} hadErrorReplyFailureOrSkip=${hadErrorReplyFailureOrSkip}`);

---

{ messages: { statusReactions: { enabled: true, emojis: { error: "👍" } } } }
RAW_BUFFERClick to expand / collapse

Problem

When a user sends any prompt to the Telegram bot — even the simplest one with no tools and no deep reasoning — the status reaction sequence ends on the error emoji (😱 by default) and persists, instead of transitioning to done (👍). The user receives the correct reply text; this is purely a reaction-indicator issue.

Observed sequence

For prompt was ist 2026 geteilt durch 11? (pure inference, no tools):

  1. 👀 (ack / queued) — appears instantly
  2. 🤔 (thinking) — during reasoning
  3. [final text reply delivered to the user: 184, correct]
  4. 😱 (error) — appears after the text arrived
  5. 😱 persists (without removeAckAfterReply) or clears after errorHoldMs (with removeAckAfterReply: true — though see caveat at bottom)

Expected: step 4 should be 👍 (done).

Environment

  • OpenClaw 2026.4.14 (arm64 Linux, Raspberry Pi 5, Node 22.22.1)
  • Default Telegram account (DM with bot, no topics)
  • Model: openai-codex/gpt-5.4 via Codex OAuth (ChatGPT Plus subscription)
  • messages.statusReactions.enabled: true (using default emojis — all Telegram-whitelisted)
  • messages.ackReaction: "👀", messages.ackReactionScope: "all"
  • channels.telegram.streaming.mode: "block" (also reproduces with "partial" and "off")

Reproduction is deterministic — fires on every turn, not intermittent.

Captured evidence (debug-level run)

Captured a turn at --log-level debug for the prompt above (20:20 CEST, message_id=4410). Relevant log lines:

20:20:32.140 [agent/embedded] embedded run start: runId=10906df2-... provider=openai-codex model=gpt-5.4 thinking=medium messageChannel=telegram
20:20:33.148 [agent/embedded] [context-diag] pre-prompt: messages=82 promptChars=318
20:20:39.231 [diagnostic] session state: prev=processing new=idle reason="run_completed" queueDepth=0
20:20:39.520 [diagnostic] lane task done: lane=session:agent:main:telegram:direct:7169013046 durationMs=9261 active=0 queued=0
20:20:40.197 [gateway/channels/telegram] telegram sendMessage ok chat=7169013046 message=4410
20:20:40.199 telegram ingress: chatId=7169013046 dispatchCompleteMs=12292 buffer=inbound-debounce

Key observations:

  • Run completed cleanly: reason="run_completed", active=0 queued=0, durationMs=9261 — not aborted, not timed out
  • Message delivered successfully: telegram sendMessage ok on message=4410
  • No error log lines during this turn (no dispatch failed, no throw, no timeout)
  • Yet the user observed 😱 as the final reaction, which can only come from setError() — meaning !hasFinalResponse was truthy at the finalize check in bot-BwMz6R6-.js:5086

This is the smoking gun: a successful, non-aborted, clean run with confirmed delivery still ends on setError().

Root-cause hypothesis

In extensions/telegram/src/bot/*.ts (compiled to dist/bot-BwMz6R6-.js:5075-5130), the reaction-finalize logic is:

const deliverySummary = deliveryState.snapshot();
if (dispatchError || !deliverySummary.delivered && (...skipped/failed...)) {
  // Send fallback "Something went wrong" text
  sentFallback = (await deliverReplies({
    replies: [{ text: fallbackText }],
    silent: silentErrorReplies && (dispatchError != null || hadErrorReplyFailureOrSkip),
    ...
  })).delivered;
}
const hasFinalResponse = queuedFinal || sentFallback;

if (statusReactionController && !hasFinalResponse)
  Promise.resolve(statusReactionController.setError())...  // ← sets 😱

if (!hasFinalResponse) {
  clearGroupHistory();
  return;
}

// ... auto-topic-label code ...

if (statusReactionController)
  Promise.resolve(statusReactionController.setDone())...  // ← sets 👍

The intent is correct: setError only when no final response was delivered. But in practice it ends up calling setError even when the user received the reply. Likely candidate paths:

Hypothesis A — queuedFinal never flips to true on streamed/block deliveries The deliver callback in the dispatcher (around line 4885) only sets queuedFinal = true inside the shouldSuppressLocalTelegramExecApprovalPrompt branch. The "normal" flow to set it is via the return value of dispatchReplyWithBufferedBlockDispatcher (line 4877: ({queuedFinal} = await telegramDeps.dispatchReplyWithBufferedBlockDispatcher(...))). If the dispatcher returns queuedFinal: false for deliveries where only preview-updates were sent and no explicit "final" segment was marked, hasFinalResponse collapses to sentFallback.

Hypothesis B — silent fallback fires unnecessarily If dispatchError is truthy (maybe set transiently by a rate-limit retry or hadErrorReplyFailureOrSkip flipping on a preview-edit race), the fallback path runs. The fallback message is silent: true when silentErrorReplies is on, and might be suppressed or skipped. In that case sentFallback = false, and combined with queuedFinal = false, hasFinalResponse = false → setError.

Hypothesis C — hadErrorReplyFailureOrSkip triggered by a non-critical payload flag Line 4884: if (payload.isError === true) hadErrorReplyFailureOrSkip = true; — any streamed payload with isError: true (even a transient-recoverable one, or a reasoning-lane marker) sets the flag for the whole turn.

Diagnostic I would add

Add a consolidated debug log line around bot-BwMz6R6-.js:5085 (source: extensions/telegram/src/bot/*.ts):

logVerbose(`telegram: reaction finalize: queuedFinal=${queuedFinal} sentFallback=${sentFallback} dispatchError=${Boolean(dispatchError)} deliverySummary=${JSON.stringify(deliverySummary)} hadErrorReplyFailureOrSkip=${hadErrorReplyFailureOrSkip}`);

Right now those four flags aren't logged together anywhere, so even at debug level the root cause isn't directly visible. With this line, one debug-run on a trivial prompt decisively pins down which hypothesis holds.

Proposed fix (pending root-cause confirmation)

Depending on which hypothesis the debug log confirms:

  • If Hypothesis A: ensure queuedFinal = true is set in the main streaming/block dispatch path whenever a non-empty answer-lane segment is delivered and confirmed by deliverLaneText to have reached the user.
  • If Hypothesis B: decouple dispatchError from the fallback decision — only send fallback if !deliverySummary.delivered, not on dispatchError alone.
  • If Hypothesis C: scope hadErrorReplyFailureOrSkip per-lane, so a reasoning-lane error marker doesn't poison the final answer-lane success.

Test Plan

  1. Run trivial prompt (2+2 or similar) at --log-level debug. Observe logged flag state at finalize.
  2. Run tool-heavy prompt (e.g. read some file) same way. Compare.
  3. Run prompt in different streaming modes (off, partial, block). Does hypothesis A hold per-mode?
  4. After fix: assert that on a successful trivial turn, queuedFinal=true, hasFinalResponse=true, setDone() is called, setError() is NOT called.

Workaround for affected users

Three options:

  1. Accept and ignore — the error emoji is purely cosmetic, delivery works.
  2. Override the error emoji to a neutral/positive symbol (e.g. 👍 or 🤝). All turns will then end with the same emoji regardless of setError vs setDone, at the cost of losing visual distinction for genuinely failed turns:
    { messages: { statusReactions: { enabled: true, emojis: { error: "👍" } } } }
  3. Disable status reactionsmessages.statusReactions.enabled: false. Keeps the raw ackReaction but no in-turn transitions.

Related observations

  • Zero editMessageText calls observed across a full day of operation with the default Telegram bot using openai-codex/gpt-5.4 (Codex OAuth). 62+ sendMessage ok entries vs 0 editMessageText entries. Streaming config (mode: partial|block) is accepted by the schema but effectively never triggers preview-editing. Hypothesis: Codex OAuth provider does not emit token-by-token stream deltas to the channel bridge, so the Telegram preview-edit path (bot-BwMz6R6-.js:417-420) is never exercised. Either (a) Codex OAuth uses a non-streaming response shape for this model, or (b) the gateway streaming-hook → channel-preview wiring has a bug in this provider path. Possibly a separate issue; flagging here because it's the same bot + same turn as the main bug.

  • removeAckAfterReply: true has no effect in the Telegram path when statusReactionController is active. In bot-BwMz6R6-.js:5125-5130, the removeAckReactionAfterReply({... removeAfterReply: removeAckAfterReply ...}) call sits in an else branch that only executes when statusReactionController is null. When status reactions are enabled (the common case), the final emoji from setDone/setError is never explicitly cleared. This is related to the main bug because it means the error emoji persists instead of auto-clearing after errorHoldMs. Could fold into this issue or keep as separate.

  • active-memory plugin times out consistently at its configured timeoutMs: 15000 returning summaryChars=0. Out of scope here; could warrant its own issue.

Metadata

  • Observed 2026-04-21
  • OpenClaw version: 2026.4.14
  • Gateway / systemd service (running under systemd --user)
  • Not reproduced on other channels (this user only uses Telegram)

Severity: Low — user-visible inconsistency but no functional impact on message delivery.

extent analysis

TL;DR

The most likely fix involves modifying the reaction-finalize logic in the Telegram bot to correctly set hasFinalResponse based on the delivery state, ensuring that setError() is not called when a final response is delivered.

Guidance

  1. Verify the root cause: Add a consolidated debug log line around bot-BwMz6R6-.js:5085 to log the states of queuedFinal, sentFallback, dispatchError, deliverySummary, and hadErrorReplyFailureOrSkip together.
  2. Investigate Hypothesis A: Check if queuedFinal is set to true in the main streaming/block dispatch path for non-empty answer-lane segments.
  3. Review Hypothesis B: Examine if dispatchError is being set unnecessarily, causing the fallback path to run and hasFinalResponse to be false.
  4. Examine Hypothesis C: Determine if hadErrorReplyFailureOrSkip is being triggered by a non-critical payload flag, affecting the final response.

Example

No code example is provided as the issue requires further investigation to determine the root cause.

Notes

The provided information suggests that the issue is related to the reaction-finalize logic in the Telegram bot, but the exact cause needs to be confirmed through debugging. The proposed fixes depend on which hypothesis is confirmed by the debug log.

Recommendation

Apply a workaround, such as overriding the error emoji to a neutral symbol (e.g., 👍) or disabling status reactions, until the root cause is confirmed and a fix is implemented. This will mitigate the user-visible inconsistency without affecting message delivery.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix Telegram status reaction ends on error emoji (😱) despite successful message delivery — Codex OAuth [1 pull requests, 1 participants]