From dc6afeb4f882d3ff875f6444af07e9227f0a0479 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 22 Feb 2026 21:16:05 +0100 Subject: [PATCH] perf(webchat): skip unnecessary full history reloads on final events (#20588) Co-authored-by: amzzzzzzz <154392693+amzzzzzzz@users.noreply.github.com> --- CHANGELOG.md | 1 + ui/src/ui/app-gateway.ts | 3 +- ui/src/ui/chat-event-reload.test.ts | 47 +++++++++++++++++++++++++++++ ui/src/ui/chat-event-reload.ts | 16 ++++++++++ 4 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 ui/src/ui/chat-event-reload.test.ts create mode 100644 ui/src/ui/chat-event-reload.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c114d97fe1..1de5d838f9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ Docs: https://docs.openclaw.ai - Control UI/WebSocket: stop and clear the browser gateway client on UI teardown so remounts cannot leave orphan websocket clients that create duplicate active connections. (#23422) Thanks @floatinggball-design. - Webchat/Chat: apply assistant `final` payload messages directly to chat state so sent turns render without waiting for a full history refresh cycle. (#14928) Thanks @BradGroux. - Webchat/Chat: for out-of-band final events (for example tool-call side runs), append provided final assistant payloads directly instead of forcing a transient history reset. (#11139) Thanks @AkshayNavle. +- Webchat/Performance: reload `chat.history` after final events only when the final payload lacks a renderable assistant message, avoiding expensive full-history refreshes on normal turns. (#20588) Thanks @amzzzzzzz. - Config/Memory: allow `"mistral"` in `agents.defaults.memorySearch.provider` and `agents.defaults.memorySearch.fallback` schema validation. (#14934) Thanks @ThomsenDrake. - Security/Feishu: enforce ID-only allowlist matching for DM/group sender authorization, normalize Feishu ID prefixes during checks, and ignore mutable display names so display-name collisions cannot satisfy allowlist entries. This ships in the next npm release. Thanks @jiseoung for reporting. - Security/Group policy: harden `channels.*.groups.*.toolsBySender` matching by requiring explicit sender-key types (`id:`, `e164:`, `username:`, `name:`), preventing cross-identifier collisions across mutable/display-name fields while keeping legacy untyped keys on a deprecated ID-only path. This ships in the next npm release. Thanks @jiseoung for reporting. diff --git a/ui/src/ui/app-gateway.ts b/ui/src/ui/app-gateway.ts index 338c3b5806c..4b2b0748416 100644 --- a/ui/src/ui/app-gateway.ts +++ b/ui/src/ui/app-gateway.ts @@ -12,6 +12,7 @@ import { } from "./app-settings.ts"; import { handleAgentEvent, resetToolStream, type AgentEventPayload } from "./app-tool-stream.ts"; import type { OpenClawApp } from "./app.ts"; +import { shouldReloadHistoryForFinalEvent } from "./chat-event-reload.ts"; import { loadAgents } from "./controllers/agents.ts"; import { loadAssistantIdentity } from "./controllers/assistant-identity.ts"; import { loadChatHistory } from "./controllers/chat.ts"; @@ -256,7 +257,7 @@ function handleGatewayEventUnsafe(host: GatewayHost, evt: GatewayEventFrame) { } } } - if (state === "final") { + if (state === "final" && shouldReloadHistoryForFinalEvent(payload)) { void loadChatHistory(host as unknown as OpenClawApp); } return; diff --git a/ui/src/ui/chat-event-reload.test.ts b/ui/src/ui/chat-event-reload.test.ts new file mode 100644 index 00000000000..278a1a5994c --- /dev/null +++ b/ui/src/ui/chat-event-reload.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from "vitest"; +import { shouldReloadHistoryForFinalEvent } from "./chat-event-reload.ts"; + +describe("shouldReloadHistoryForFinalEvent", () => { + it("returns false for non-final events", () => { + expect( + shouldReloadHistoryForFinalEvent({ + runId: "run-1", + sessionKey: "main", + state: "delta", + message: { role: "assistant", content: [{ type: "text", text: "x" }] }, + }), + ).toBe(false); + }); + + it("returns true when final event has no message payload", () => { + expect( + shouldReloadHistoryForFinalEvent({ + runId: "run-1", + sessionKey: "main", + state: "final", + }), + ).toBe(true); + }); + + it("returns false when final event includes assistant payload", () => { + expect( + shouldReloadHistoryForFinalEvent({ + runId: "run-1", + sessionKey: "main", + state: "final", + message: { role: "assistant", content: [{ type: "text", text: "done" }] }, + }), + ).toBe(false); + }); + + it("returns true when final event message role is non-assistant", () => { + expect( + shouldReloadHistoryForFinalEvent({ + runId: "run-1", + sessionKey: "main", + state: "final", + message: { role: "user", content: [{ type: "text", text: "echo" }] }, + }), + ).toBe(true); + }); +}); diff --git a/ui/src/ui/chat-event-reload.ts b/ui/src/ui/chat-event-reload.ts new file mode 100644 index 00000000000..2eb211d01aa --- /dev/null +++ b/ui/src/ui/chat-event-reload.ts @@ -0,0 +1,16 @@ +import type { ChatEventPayload } from "./controllers/chat.ts"; + +export function shouldReloadHistoryForFinalEvent(payload?: ChatEventPayload): boolean { + if (!payload || payload.state !== "final") { + return false; + } + if (!payload.message || typeof payload.message !== "object") { + return true; + } + const message = payload.message as Record; + const role = typeof message.role === "string" ? message.role.toLowerCase() : ""; + if (role && role !== "assistant") { + return true; + } + return false; +}