Skip to content

fix(gateway): persist streamed text when webchat final event lacks message#31920

Merged
steipete merged 2 commits intoopenclaw:mainfrom
Sid-Qin:fix/31895-webchat-streaming-persist
Mar 2, 2026
Merged

fix(gateway): persist streamed text when webchat final event lacks message#31920
steipete merged 2 commits intoopenclaw:mainfrom
Sid-Qin:fix/31895-webchat-streaming-persist

Conversation

@Sid-Qin
Copy link
Contributor

@Sid-Qin Sid-Qin commented Mar 2, 2026

Summary

  • Problem: In the webchat interface, when an agent streams text then immediately runs tool calls, the streamed content is lost — the "final" event arrives with `message: undefined` (buffer consumed by sub-run), and the client clears `chatStream` without saving it.
  • Why it matters: Users see a thoughtful multi-paragraph response streaming in, start reading it, then it vanishes and gets replaced. This is the most-reported UX frustration in webchat.
  • What changed: Before clearing `chatStream` on a "final" event, check if the stream buffer has non-empty content. If no `finalMessage` was provided, synthesize an assistant message from the buffered text — mirroring the existing "aborted" handler's preservation logic.
  • What did NOT change: Server-side streaming, the "final" event emission, or the behavior when a proper `finalMessage` is present.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

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

Linked Issue/PR

User-visible / Behavior Changes

  • Streamed text responses in webchat are no longer lost when tool calls follow immediately.
  • The streamed content is persisted as a chat message before the stream state is cleared.

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: Any
  • Runtime: Node.js + Browser
  • Integration/channel: Webchat

Steps

  1. Open webchat session.
  2. Ask a question that triggers text response followed by tool calls (e.g. "explain X and then search for Y").
  3. Agent streams a text response, then immediately calls tools.

Expected

  • Streamed text persists in chat; tool results appear below.

Actual

  • Before fix: Streamed text vanishes, replaced by post-tool-call response.
  • After fix: Streamed text is saved as a message before clearing the stream.

Evidence

Client-side change follows the same pattern as the existing "aborted" handler which already persists streamed text. Added 4 tests: persist on missing finalMessage, skip empty/whitespace streams, skip null stream, prefer finalMessage over stream.

Human Verification (required)

  • Verified scenarios: text → tool call → final with no message, text → final with message, abort with stream
  • Edge cases checked: empty stream, whitespace-only stream, null stream, finalMessage takes priority
  • What I did not verify: Multiple sequential tool calls within a single turn, sub-agent streaming

Compatibility / Migration

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

Failure Recovery (if this breaks)

  • How to disable/revert: Revert the `else if` block in the "final" handler
  • Files/config to restore: `ui/src/ui/controllers/chat.ts`
  • Known bad symptoms: If broken, duplicate messages could appear (both stream text and final message)

Risks and Mitigations

  • Risk: Duplicate messages when both stream and finalMessage exist — Mitigated: `else if` ensures only one path executes
  • Risk: Empty/whitespace stream persisted — Mitigated: guard checks `state.chatStream?.trim()`

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 2, 2026

Greptile Summary

Added client-side stream persistence logic to prevent loss of streamed text in webchat when tool calls follow immediately. The change mirrors the existing "aborted" handler's pattern.

Key Changes:

  • Modified chat.ts final event handler to persist chatStream content when finalMessage is undefined
  • Guard condition (state.chatStream?.trim()) prevents persisting empty/whitespace-only streams
  • Uses else if to ensure finalMessage always takes precedence, preventing duplicates
  • Added 4 comprehensive tests covering main use case and edge cases (empty, whitespace, null, precedence)
  • Updated vitest.config.ts to include the test file

Code Quality:

  • Follows existing pattern from "aborted" handler (lines 267-286)
  • Minimal, focused change addressing the reported UX issue
  • Good test coverage with edge case handling
  • Backward compatible, purely additive behavior

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The change is well-implemented with comprehensive test coverage, follows existing patterns in the codebase, and addresses a legitimate UX issue. The else if structure prevents duplicate messages, the trim check handles edge cases properly, and the logic mirrors the battle-tested "aborted" handler. All tests pass and cover main use case plus edge cases.
  • No files require special attention

Last reviewed commit: 52cd5cc

SidQin-cyber and others added 2 commits March 2, 2026 19:54
…ssage

When an agent streams text and then immediately runs tool calls, the
webchat UI drops the streamed content: the "final" event arrives with
message: undefined (buffer consumed by sub-run), and the client clears
chatStream without saving it to chatMessages.

Before clearing chatStream on a "final" event, check whether the stream
buffer has content. If no finalMessage was provided but the stream is
non-empty, synthesize an assistant message from the buffered text —
mirroring the existing "aborted" handler's preservation logic.

Closes openclaw#31895
@steipete steipete force-pushed the fix/31895-webchat-streaming-persist branch from 52cd5cc to a708323 Compare March 2, 2026 19:54
@steipete steipete merged commit a351ab2 into openclaw:main Mar 2, 2026
9 checks passed
@steipete
Copy link
Contributor

steipete commented Mar 2, 2026

Landed via temp rebase onto main.

  • Gate: pnpm -s vitest run ui/src/ui/controllers/chat.test.ts
  • Land commit: a708323
  • Merge commit: a351ab2

Thanks @Sid-Qin!

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

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: a708323b19

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +254 to +255
} else if (state.chatStream?.trim()) {
state.chatMessages = [

Choose a reason for hiding this comment

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

P1 Badge Prevent synthesized stream message from being overwritten

When a final chat event arrives without payload.message, this new branch appends state.chatStream locally, but handleChatEvent still returns "final", so ui/src/ui/app-gateway.ts immediately calls loadChatHistory because shouldReloadHistoryForFinalEvent returns true for missing messages; loadChatHistory replaces chatMessages wholesale, which drops the synthesized assistant message in the same text-then-tool-turn scenario this fix is targeting. Either skip that reload when we synthesize from chatStream, or persist/merge in a way that survives the reload.

Useful? React with 👍 / 👎.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Webchat streaming responses get replaced/overwritten by subsequent tool calls

2 participants