openclaw - ✅(Solved) Fix [Bug]: OpenClaw Control UI can drop live chat updates when `chat.send` uses a raw session key and gateway emits canonical session key [2 pull requests, 1 comments, 2 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#73716Fetched 2026-04-29 06:16:01
View on GitHub
Comments
1
Participants
2
Timeline
8
Reactions
0
Author
Timeline (top)
referenced ×3cross-referenced ×2labeled ×2commented ×1

Control UI live chat responses can be dropped when chat.send uses a raw session key such as main but gateway chat events use the canonical session key agent:main:main; refreshing shows the persisted reply.

Root Cause

This appears setup-dependent because it depends on the Control UI holding a raw/alias session key such as main while the gateway emits canonical session keys such as agent:main:main.

Fix Action

Fix / Workaround

A local patch that allowed the active matching runId despite the raw/canonical session-key mismatch made live UI updates work again.

PR fix notes

PR #73721: fix(ui): deliver live chat events on runId match when sessionKey differs (#73716)

Description (problem / solution / changelog)

What

Fixes #73716. Control UI's handleChatEvent previously dropped any payload whose sessionKey did not match state.sessionKey exactly:

if (payload.sessionKey !== state.sessionKey) {
  return null;
}

When the UI was holding a raw alias (e.g. main) but the gateway emitted live chat events under the canonical session key (e.g. agent:main:main), live deltas and finals for the in-flight client run were silently dropped. The user saw no live response and had to refresh the page to see the persisted reply.

Fix

Accept events when EITHER the sessionKey matches OR the active client run's runId matches the payload's runId:

const sessionMatches = payload.sessionKey === state.sessionKey;
const activeRunMatches =
  state.chatRunId != null &&
  typeof payload.runId === "string" &&
  payload.runId === state.chatRunId;
if (!sessionMatches && !activeRunMatches) {
  return null;
}

The runId check is gated on the UI having an active run (state.chatRunId != null), so sub-agent / fleet announces with a different runId still cannot leak into an unrelated session — the existing payload.runId !== state.chatRunId guard a few lines below continues to handle that case.

Test changes

  • Renamed returns null when sessionKey does not matchreturns null when sessionKey does not match and no active run is in flight to clarify the precondition (the existing assertion still holds — no active run).
  • New accepts events whose runId matches the active client run even when sessionKey differs (#73716) pins the regression: raw main UI state, gateway emits agent:main:main, runId match → delta delivered.
  • New still drops events when neither sessionKey nor runId matches belt-and-braces case pinning that the existing sub-agent/fleet announce protection is preserved.

Pre-implement audit

  1. Existing-helper check (vincentkoc #57341). No helper introduced — the active-run match uses existing state fields (state.chatRunId, payload.runId). ✅
  2. Shared-helper caller check (steipete #60623). handleChatEvent is module-scoped and called only from the chat-event subscription. The behavior change is widening the accept condition (logical OR), so callers that already match by sessionKey keep behaving identically. ✅
  3. Broader-fix rival scan (steipete #68270). Zero rival PRs reference #73716. ✅

Verified locally

npx oxlint ui/src/ui/controllers/chat.ts ui/src/ui/controllers/chat.test.ts
# Found 0 warnings and 0 errors.

npx vitest run ui/src/ui/controllers/chat.test.ts
# Tests  52 passed (52)

lobster-biscuit: 73716-control-ui-runid-fallback

Sign-Off:

  • I have read and agree to the OpenClaw Contributor License Agreement.

Changed files

  • CHANGELOG.md (modified, +4/-0)
  • ui/src/ui/controllers/chat.test.ts (modified, +63/-1)
  • ui/src/ui/controllers/chat.ts (modified, +12/-1)

PR #73534: fix(telegram): scope native quote streaming guard to real quotes (#73505)

Description (problem / solution / changelog)

What

Fixes #73505. When channels.telegram.replyToMode is set to anything other than "off", partial-stream preview is silently disabled for plain replies even though editMessageText round-trips with reply_to_message_id unchanged. The result is that operators with replyToMode: "first" see no streaming preview at all — text appears only after the run completes.

Root cause

bot-message-dispatch.ts builds a replyQuoteByMessageId map for native-quote candidate lookups during delivery, then computes:

const hasNativeQuoteReply =
  replyToMode !== "off" && Object.keys(replyQuoteByMessageId).length > 0;

Under replyToMode !== "off" the dispatcher unconditionally calls addTelegramNativeQuoteCandidate(replyQuoteByMessageId, ctxPayload.MessageSid ?? msg.message_id, ...) for the inbound message itself, so any inbound message with text populates the map and trips hasNativeQuoteReplycanStreamAnswerDraft = false → no preview lane.

The reason streaming is incompatible with native quote replies is the Telegram quote parameter, which editMessageText cannot re-send. Plain reply_to_message_id (no quote) does not have that constraint — and the dispatcher already passes draftReplyToMessageId into createTelegramDraftStream, which means the rest of the codepath is built to handle this case.

Fix

Tighten the guard to require an actual user-selected quote, matching the same replyQuoteText && replyQuoteMessageId != null pattern already used a few lines above to gate addTelegramNativeQuoteCandidate for explicit reply targets:

const hasNativeQuoteReply =
  replyToMode !== "off" && replyQuoteText != null && replyQuoteMessageId != null;

replyQuoteText is only set when ctxPayload.ReplyToIsQuote is true, and replyQuoteMessageId is only set when that quote points at a non-external resolvable message. Together they describe the only case where streaming truly cannot work.

Behavior preserved

  • Quote-bearing replies (ReplyToIsQuote=true with text + resolvable id) still take the non-streaming path — covered by the existing skips answer draft preview for same-chat selected quotes test.
  • replyQuoteByMessageId is still passed verbatim to deliverReplies for fallback handling — covered by the existing passes native quote candidates for current message replies and passes native quote candidates for explicit reply targets tests, which still assert the same map payload.

Test changes

  • passes native quote candidates for current message replies — flipped from asserting createTelegramDraftStream is NOT called to asserting it IS called. The map-payload assertion is unchanged: candidates are still attached for the eventual final delivery.
  • New regression test keeps answer draft preview for plain replies in groups with replyToMode set — pins #73505 behavior directly with replyToMode: "first" + streamMode: "partial" + plain inbound message → expects createTelegramDraftStream and draftStream.update("Hello").

Verified locally

npx oxlint extensions/telegram/src/bot-message-dispatch.ts extensions/telegram/src/bot-message-dispatch.test.ts
# Found 0 warnings and 0 errors.

npx vitest run extensions/telegram/src/bot-message-dispatch.test.ts
# Tests  109 passed (109)

npx vitest run extensions/telegram/src/bot/delivery.test.ts
# Tests  41 passed (41)

lobster-biscuit: 73505-replytomode-streaming-guard

Sign-Off:

  • I have read and agree to the OpenClaw Contributor License Agreement.

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • extensions/telegram/src/bot-message-dispatch.test.ts (modified, +45/-1)
  • extensions/telegram/src/bot-message-dispatch.ts (modified, +9/-1)

Code Example

Protocol-level evidence from local debugging:

A websocket client sent `chat.send` with raw session key:


{
  "method": "chat.send",
  "params": {
    "sessionKey": "main",
    "message": "Reply exactly: MAIN-KEY-OK",
    "deliver": false,
    "idempotencyKey": "<client-run-id>"
  }
}


The gateway acknowledged the send with the same run id, then emitted live chat events using the canonical session key:


chat state=delta eventSessionKey=agent:main:main equalsRaw=false runMatches=true
chat state=final eventSessionKey=agent:main:main equalsRaw=false runMatches=true


A separate websocket diagnostic client received the deltas/final in real time. The browser UI did not surface them until refresh, at which point `chat.history` loaded the persisted reply.

Installed/minified Control UI logic appeared equivalent to:


if (!event || event.sessionKey !== this.sessionKey) return null;


A local patch that allowed the active matching `runId` despite the raw/canonical session-key mismatch made live UI updates work again.
RAW_BUFFERClick to expand / collapse

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

No

Summary

Control UI live chat responses can be dropped when chat.send uses a raw session key such as main but gateway chat events use the canonical session key agent:main:main; refreshing shows the persisted reply.

Steps to reproduce

  1. Start OpenClaw 2026.4.25 gateway on 127.0.0.1:18789.
  2. Open the Control UI in a browser.
  3. Put the UI into the raw main session key state, for example via URL/query/localStorage if available in the current UI build.
  4. Send a chat message from the web Control UI.
  5. Observe that inference completes but the assistant reply is not shown live.
  6. Refresh the page.
  7. Observe that the assistant reply appears from persisted chat history.

Expected behavior

When a chat message is sent from the Control UI, the UI should display the assistant response live without requiring a page refresh when the gateway emits chat events for the same in-flight run.

Actual behavior

The assistant response is generated and persisted, but the Control UI does not show the live response. A websocket diagnostic client received chat delta/final events with the same runId, but the events used canonical sessionKey: "agent:main:main" while the UI/request used raw sessionKey: "main". Refreshing the page makes the assistant reply appear from persisted chat history.

OpenClaw version

2026.4.25

Operating system

WSL2 Ubuntu on Windows 11 Pro

Install method

npm global install

Model

ollama/qwen3-coder:30b

Provider / routing chain

OpenClaw -> Ollama local

Additional provider/model setup details

Ollama was running locally and the model worked directly outside the UI. The OpenClaw CLI path also worked with the same setup: openclaw agent --local --message ... --json returned replies.

The issue appears to be in Control UI live event handling, not provider/model generation. The gateway emitted live chat events and persisted the transcript.

Logs, screenshots, and evidence

Protocol-level evidence from local debugging:

A websocket client sent `chat.send` with raw session key:


{
  "method": "chat.send",
  "params": {
    "sessionKey": "main",
    "message": "Reply exactly: MAIN-KEY-OK",
    "deliver": false,
    "idempotencyKey": "<client-run-id>"
  }
}


The gateway acknowledged the send with the same run id, then emitted live chat events using the canonical session key:


chat state=delta eventSessionKey=agent:main:main equalsRaw=false runMatches=true
chat state=final eventSessionKey=agent:main:main equalsRaw=false runMatches=true


A separate websocket diagnostic client received the deltas/final in real time. The browser UI did not surface them until refresh, at which point `chat.history` loaded the persisted reply.

Installed/minified Control UI logic appeared equivalent to:


if (!event || event.sessionKey !== this.sessionKey) return null;


A local patch that allowed the active matching `runId` despite the raw/canonical session-key mismatch made live UI updates work again.

Impact and severity

Affected: Users whose Control UI session state uses a raw/alias session key such as main while the gateway emits canonical session keys such as agent:main:main.

Severity: Medium. Chat generation completes and history is persisted, but the web UI appears stuck or incomplete until the user manually refreshes.

Frequency: Observed when the UI/request session key was main and live gateway events used agent:main:main. I do not know how common this state is across installs.

Consequence: Users may think the web chat failed even though the model completed successfully, leading to repeated sends, missed live responses, and a confusing setup/onboarding experience.

Additional information

I do not know whether this is a regression. I only observed it on OpenClaw 2026.4.25.

This appears setup-dependent because it depends on the Control UI holding a raw/alias session key such as main while the gateway emits canonical session keys such as agent:main:main.

The model/provider is probably not relevant: Ollama worked directly, the OpenClaw CLI path returned replies, the gateway emitted live chat events, and the transcript was persisted.

Suggested fix: have the UI accept live chat events for the active in-flight runId even when the event session key differs from the current raw session key, or normalize the UI to the canonical session key returned by the gateway.

extent analysis

TL;DR

The issue can be fixed by modifying the Control UI to accept live chat events for the active in-flight runId even when the event session key differs from the current raw session key.

Guidance

  • The likely cause is the mismatch between the raw session key used by the UI (main) and the canonical session key used by the gateway (agent:main:main).
  • To verify, check the websocket diagnostic client output to confirm that the gateway is emitting live chat events with the canonical session key.
  • To mitigate, modify the UI logic to allow live updates when the runId matches, regardless of the session key mismatch, as shown in the local patch.
  • Consider normalizing the UI to use the canonical session key returned by the gateway to prevent similar issues in the future.

Example

// Modified UI logic to allow live updates when runId matches
if (!event || event.runId !== this.runId) return null;

Notes

This fix assumes that the runId is unique and can be used to match the live chat events with the UI request. If this is not the case, additional modifications may be needed.

Recommendation

Apply the workaround by modifying the UI logic to accept live chat events for the active in-flight runId even when the event session key differs from the current raw session key. This will allow the UI to display live responses without requiring a page refresh.

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…

FAQ

Expected behavior

When a chat message is sent from the Control UI, the UI should display the assistant response live without requiring a page refresh when the gateway emits chat events for the same in-flight run.

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 [Bug]: OpenClaw Control UI can drop live chat updates when `chat.send` uses a raw session key and gateway emits canonical session key [2 pull requests, 1 comments, 2 participants]