fix: harden chat history lifecycle edges

This commit is contained in:
Shakker
2026-07-15 01:43:35 +01:00
committed by Shakker
parent f310689cb0
commit a2f6d96bfb
4 changed files with 69 additions and 2 deletions

View File

@@ -732,6 +732,7 @@ describe("chat pane catalog session lifecycle", () => {
pane.hasOlderMessages = vi.fn(() => true);
pane.loadOlderMessages = vi.fn(async () => undefined);
vi.stubGlobal("IntersectionObserver", undefined);
vi.stubGlobal("TouchEvent", undefined);
const thread = document.createElement("div");
const event = new WheelEvent("wheel", { deltaY: -1 });
Object.defineProperty(event, "currentTarget", { value: thread });
@@ -825,6 +826,27 @@ describe("chat pane native history pagination", () => {
}
});
it("does not consume bootstrap history while disconnected", () => {
const client = { request: vi.fn() } as unknown as GatewayBrowserClient;
const { pane, state } = createTestChatPane({ client, sessions: {} as SessionCapability });
state.connected = false;
state.chatHistoryPagination = { hasMore: true, nextOffset: 2, totalMessages: 4 };
const construct = vi.fn();
class FakeIntersectionObserver {
constructor() {
construct();
}
disconnect() {}
observe() {}
}
vi.stubGlobal("IntersectionObserver", FakeIntersectionObserver);
pane.syncHistoryObserver();
expect(construct).not.toHaveBeenCalled();
expect(pane.historyAutoLoadBlocked).toBe(false);
});
it("stops non-scrollable bootstrap after one older page", async () => {
const request = vi.fn(async () => ({
messages: [nativeHistoryMessage(3), nativeHistoryMessage(4)],

View File

@@ -979,6 +979,7 @@ class ChatPane extends OpenClawLightDomElement {
}
if (
typeof IntersectionObserver !== "function" ||
!this.state?.connected ||
this.loadingOlder ||
!this.hasOlderMessages()
) {
@@ -1089,7 +1090,7 @@ class ChatPane extends OpenClawLightDomElement {
let upward =
(event instanceof WheelEvent && event.deltaY < 0) ||
(event instanceof KeyboardEvent && CHAT_HISTORY_UPWARD_KEYS.has(event.key));
if (event instanceof TouchEvent) {
if (typeof TouchEvent !== "undefined" && event instanceof TouchEvent) {
const touchY = event.touches[0]?.clientY ?? null;
if (event.type === "touchstart") {
this.historyTouchY = touchY;

View File

@@ -148,6 +148,42 @@ describe("session message cache", () => {
expect(snapshot?.pagination).toEqual({ hasMore: false, totalMessages: 140 });
});
it("keeps the newer same-depth snapshot when a stale pane saves later", () => {
const host = createHost();
const cache: ChatMessageCache = new Map();
const current = [1, 2, 3].map((seq) => ({
content: `current-${seq}`,
__openclaw: { seq },
}));
cacheChatSessionSnapshot(
cache,
host,
{ sessionKey: "home" },
{
messages: current,
pagination: { hasMore: false, totalMessages: 3 },
sessionId: "session-1",
},
);
cacheChatSessionSnapshot(
cache,
host,
{ sessionKey: "home" },
{
messages: current.slice(0, 2),
pagination: { hasMore: true, nextOffset: 2, totalMessages: 3 },
sessionId: "session-1",
},
);
expect(readChatSessionSnapshot(cache, host, { sessionKey: "home" })).toEqual({
messages: current,
pagination: { hasMore: false, totalMessages: 3 },
sessionId: "session-1",
});
});
it("does not retain history across backing session changes", () => {
const host = createHost();
const cache: ChatMessageCache = new Map();

View File

@@ -205,6 +205,15 @@ function mergeRetainedSessionDepth(
const incomingBounds = transcriptSequenceBounds(incoming.messages);
const existingTotal = existing.pagination.totalMessages;
const incomingTotal = incoming.pagination.totalMessages;
if (
existingBounds &&
incomingBounds &&
typeof existingTotal === "number" &&
incomingTotal === existingTotal &&
incomingBounds.newest < existingBounds.newest
) {
return existing;
}
if (
!existingBounds ||
!incomingBounds ||
@@ -212,7 +221,6 @@ function mergeRetainedSessionDepth(
typeof incomingTotal !== "number" ||
incomingTotal < existingTotal ||
incomingBounds.oldest <= existingBounds.oldest ||
incomingBounds.newest < existingBounds.newest ||
incomingBounds.oldest > existingBounds.newest + 1
) {
return incoming;