From fc1e412dbef29ee1ef4f5ec29db025d5af726da0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E9=87=8C=E5=8D=BF?= Date: Fri, 24 Jul 2026 14:39:55 +0800 Subject: [PATCH] fix(chat): page history by compound cursor, not bare timestamp The agent chat view derived its "before" pagination cursor from `messages[0].created_at` alone, dropping the `|id` component. The backend's `before` contract is `|`, and it already returns a ready-to-use `cursor` field per message. When a single turn persists a batch of messages that share one `created_at` (e.g. a burst of tool_call rows) and that batch is larger than HISTORY_PAGE_SIZE, the timestamp-only cursor never advances: the backend's legacy-cursor fallback pins the id to the max UUID, so every page re-returns the same equal-timestamp rows. Pagination loops, inflating the tool count on each scroll, and any message older than the batch (including the user's first message) can never be loaded. Round-trip the backend `cursor` field at all four cursor-set sites, falling back to `created_at` only when absent. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../pages/agent-detail/AgentDetailPage.tsx | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/frontend/src/pages/agent-detail/AgentDetailPage.tsx b/frontend/src/pages/agent-detail/AgentDetailPage.tsx index ad75ec881..f4bab561c 100644 --- a/frontend/src/pages/agent-detail/AgentDetailPage.tsx +++ b/frontend/src/pages/agent-detail/AgentDetailPage.tsx @@ -2428,8 +2428,11 @@ export default function AgentDetailPage() { }), })); setChatMessages(parsed); + // Round-trip the backend's compound `|` cursor. A bare + // created_at cannot page past a batch of messages that share one timestamp + // (e.g. a burst of tool_call rows), so older messages would never load. setChatOldestTimestamp( - messages.length > 0 ? messages[0].created_at : null, + messages.length > 0 ? (messages[0].cursor ?? messages[0].created_at) : null, ); setChatHistoryHasMore(messages.length >= HISTORY_PAGE_SIZE); setMessagesLoadedRuntimeKey(buildSessionRuntimeKey(agentId, sessionId)); @@ -2753,8 +2756,9 @@ export default function AgentDetailPage() { }), })); - // Set the oldest message timestamp for cursor-based pagination - const oldestTimestamp = msgs.length > 0 ? msgs[0].created_at : null; + // Set the oldest message cursor for pagination. Use the backend's compound + // `|` cursor so a batch of equal-timestamp messages can be paged past. + const oldestTimestamp = msgs.length > 0 ? (msgs[0].cursor ?? msgs[0].created_at) : null; if (writable) { setChatMessages(preParsed); @@ -3991,8 +3995,9 @@ export default function AgentDetailPage() { const el = historyContainerRef.current; const oldScrollHeight = el?.scrollHeight ?? 0; setHistoryMsgs(prev => [...preParsed, ...prev]); - // Update the oldest timestamp (first message in the new batch, since messages are in chronological order) - setHistoryOldestTimestamp(msgs[0].created_at); + // Update the oldest cursor (first message in the new batch, since messages are in chronological order). + // Round-trip the compound `|` cursor so equal-timestamp batches don't stall pagination. + setHistoryOldestTimestamp(msgs[0].cursor ?? msgs[0].created_at); setHistoryHasMore(msgs.length >= HISTORY_PAGE_SIZE); // Restore scroll position after new messages are prepended requestAnimationFrame(() => { @@ -4040,8 +4045,9 @@ export default function AgentDetailPage() { const el = chatContainerRef.current; const oldScrollHeight = el?.scrollHeight ?? 0; setChatMessages(prev => [...preParsed, ...prev]); - // Update the oldest timestamp (first message in the new batch, since messages are in chronological order) - setChatOldestTimestamp(msgs[0].created_at); + // Update the oldest cursor (first message in the new batch, since messages are in chronological order). + // Round-trip the compound `|` cursor so equal-timestamp batches don't stall pagination. + setChatOldestTimestamp(msgs[0].cursor ?? msgs[0].created_at); setChatHistoryHasMore(msgs.length >= HISTORY_PAGE_SIZE); // Restore scroll position after new messages are prepended requestAnimationFrame(() => {