diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c114d97f..1de5d838f 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 338c3b580..4b2b07484 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 000000000..278a1a599 --- /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 000000000..2eb211d01 --- /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; +}