- Shipped
- 2 Septemba 2026, 08:48 UTC
- Author
- Kamo
- Commit
- 83e30bc
A member started a chat about KB articles, minimized it to its hexhead, went to their mail and brought the window back to a blank conversation — "having to start over". Nothing was lost: both questions and both answers were in ai_chat_messages the whole time, and no new session was ever created, which is the tell. The window came back pointing at the right conversation and could not read it. AiChatController.getMessages answers with an envelope — { messages, total, hasMore } — oldest-first, holding the newest `limit` rows. This read it as a bare AiChatMessage[]. `data.length` on an object is undefined, `data.reverse()` is not a function, and the catch swallowed the TypeError: the list stayed empty, `initialLoaded` never flipped, and the panel rendered nothing at all — no transcript, no empty-conversation panel, no error. A blank window over a transcript that was perfectly safe. It hid for as long as it did because a LIVE window never makes this call. Every message on screen gets there through the local echo in AiChatWebSocketContext, appended straight onto the kept-alive list. The read only matters when the transcript has to come back from the server: after a full document load — the tool-window snapshot restores the window WITH its session guid, hence no new session row — or a reopen past the five-minute keep-alive. Exactly the two ways a member reaches an AI window they were already using. So read the envelope, keep the service's order instead of reversing what it already reversed, and take `hasMore` from the answer rather than inferring it from a length that was never there. Paging goes with it. getMessages declares a `before` parameter and never reads it — every call is PageRequest.of(0, limit) — so the scroll-to-top a second copy of them. It never fired before (it needs `initialLoaded`, which never became true), so fixing the read would have switched it on. Older history is now one BIGGER page, which is the request the service actually serves. Not while a send is in flight: a page replaces the list, and the member's own message is a local echo until the reply lands. And a failed read now says so, with a retry, instead of rendering the void that made this look like a wiped chat. Never "start a conversation" over one that is sitting in the database — a transcript already on screen is left alone when a re-read fails. Both strings already existed in all 22 locales. The in-flight guard moves from `loading` state to a ref. It was in the useCallback deps while the initial-load effect is keyed on the session alone, so the effect held whichever copy existed when the guid arrived.