fix(webchat): drop stale optimistic assistant tails

This commit is contained in:
Peter Steinberger
2026-04-27 20:45:14 +01:00
parent 6dc8bd8935
commit 8cddb6ce7d
2 changed files with 73 additions and 8 deletions

View File

@@ -0,0 +1,54 @@
import { describe, expect, it } from "vitest";
import { preserveOptimisticTailMessages } from "../controllers/chat.ts";
describe("preserveOptimisticTailMessages", () => {
it("keeps optimistic tail messages while history is stale", () => {
const persistedUser = {
role: "user",
content: [{ type: "text", text: "first" }],
__openclaw: { seq: 1 },
};
const optimisticUser = {
role: "user",
content: [{ type: "text", text: "latest ask" }],
timestamp: 10,
};
const optimisticAssistant = {
role: "assistant",
content: [{ type: "text", text: "latest answer" }],
timestamp: 11,
};
expect(
preserveOptimisticTailMessages(
[persistedUser],
[persistedUser, optimisticUser, optimisticAssistant],
),
).toEqual([persistedUser, optimisticUser, optimisticAssistant]);
});
it("drops streamed assistant tail when final history has caught up past the shared user", () => {
const persistedUser = {
role: "user",
content: [{ type: "text", text: "latest ask" }],
__openclaw: { seq: 1 },
};
const streamedAssistant = {
role: "assistant",
content: [{ type: "text", text: "partial streamed answer" }],
timestamp: 10,
};
const historyAssistant = {
role: "assistant",
content: [{ type: "text", text: "complete persisted answer" }],
__openclaw: { seq: 2 },
};
expect(
preserveOptimisticTailMessages(
[persistedUser, historyAssistant],
[persistedUser, streamedAssistant],
),
).toEqual([persistedUser, historyAssistant]);
});
});

View File

@@ -264,7 +264,7 @@ function messageDisplaySignature(message: unknown): string | null {
}
}
function preserveOptimisticTailMessages(
export function preserveOptimisticTailMessages(
historyMessages: unknown[],
previousMessages: unknown[],
): unknown[] {
@@ -279,29 +279,37 @@ function preserveOptimisticTailMessages(
? previousMessages
: historyMessages;
}
const historySignatures = new Set(
historyMessages
.map((message) => messageDisplaySignature(message))
.filter((signature): signature is string => Boolean(signature)),
);
const historySignatureIndexes = new Map<string, number>();
historyMessages.forEach((message, index) => {
const signature = messageDisplaySignature(message);
if (signature) {
historySignatureIndexes.set(signature, index);
}
});
let sharedPreviousIndex = -1;
let sharedHistoryIndex = -1;
for (let index = previousMessages.length - 1; index >= 0; index--) {
const signature = messageDisplaySignature(previousMessages[index]);
if (signature && historySignatures.has(signature)) {
const historyIndex = signature ? historySignatureIndexes.get(signature) : undefined;
if (typeof historyIndex === "number") {
sharedPreviousIndex = index;
sharedHistoryIndex = historyIndex;
break;
}
}
if (sharedPreviousIndex < 0) {
return historyMessages;
}
if (sharedHistoryIndex < historyMessages.length - 1) {
return historyMessages;
}
const optimisticTail: unknown[] = [];
for (const message of previousMessages.slice(sharedPreviousIndex + 1)) {
if (!isLocallyOptimisticHistoryMessage(message) || shouldHideHistoryMessage(message)) {
return historyMessages;
}
const signature = messageDisplaySignature(message);
if (!signature || historySignatures.has(signature)) {
if (!signature || historySignatureIndexes.has(signature)) {
return historyMessages;
}
optimisticTail.push(message);
@@ -544,6 +552,9 @@ export async function sendChatMessage(
if (!msg && !hasAttachments) {
return null;
}
if (state.chatSending) {
return state.chatRunId;
}
const now = Date.now();