fix(webchat): append out-of-band final payloads in active chat (#11139)

Co-authored-by: AkshayNavle <110360+AkshayNavle@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-02-22 21:14:25 +01:00
parent 8264d4521b
commit f2e9986813
3 changed files with 26 additions and 2 deletions

View File

@@ -43,6 +43,7 @@ Docs: https://docs.openclaw.ai
- Webchat/Sessions: preserve existing session `label` across `/new` and `/reset` rollovers so reset sessions remain discoverable in session history lists. (#23755) Thanks @ThunderStormer.
- 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.
- 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.

View File

@@ -53,7 +53,7 @@ describe("handleChatEvent", () => {
expect(state.chatStream).toBe("Hello");
});
it("returns final for final from another run without clearing active stream", () => {
it("appends final payload from another run without clearing active stream", () => {
const state = createState({
sessionKey: "main",
chatRunId: "run-user",
@@ -69,10 +69,28 @@ describe("handleChatEvent", () => {
content: [{ type: "text", text: "Sub-agent findings" }],
},
};
expect(handleChatEvent(state, payload)).toBe("final");
expect(handleChatEvent(state, payload)).toBe(null);
expect(state.chatRunId).toBe("run-user");
expect(state.chatStream).toBe("Working...");
expect(state.chatStreamStartedAt).toBe(123);
expect(state.chatMessages).toHaveLength(1);
expect(state.chatMessages[0]).toEqual(payload.message);
});
it("returns final for another run when payload has no message", () => {
const state = createState({
sessionKey: "main",
chatRunId: "run-user",
chatStream: "Working...",
chatStreamStartedAt: 123,
});
const payload: ChatEventPayload = {
runId: "run-announce",
sessionKey: "main",
state: "final",
};
expect(handleChatEvent(state, payload)).toBe("final");
expect(state.chatRunId).toBe("run-user");
expect(state.chatMessages).toEqual([]);
});

View File

@@ -209,6 +209,11 @@ export function handleChatEvent(state: ChatState, payload?: ChatEventPayload) {
// See https://github.com/openclaw/openclaw/issues/1909
if (payload.runId && state.chatRunId && payload.runId !== state.chatRunId) {
if (payload.state === "final") {
const finalMessage = normalizeFinalAssistantMessage(payload.message);
if (finalMessage) {
state.chatMessages = [...state.chatMessages, finalMessage];
return null;
}
return "final";
}
return null;