Skip to content

Commit 9986e4b

Browse files
olafuraclaude
andcommitted
fix: invalidate lazy-loaded older history when the live window reshapes
Address review (PR pingdotgg#3510, Cursor Bugbot): - Reconnect / checkpoint revert can re-snapshot or filter the live activity window, but the prepended `olderActivities` weren't invalidated — leaving gaps or showing reverted history. Reset the lazy-load state when the live window's oldest activity id changes (it's stable while activities only append, so this doesn't fire during a normal turn), in addition to on thread switch. Web + mobile. - Web only deduped a new older page against already-loaded older pages, not the live window (mobile already did both). Dedup against both so a boundary overlap can't produce duplicate ids / React keys. Not changed — the "unsequenced cursor hides sequenced history" finding is a false positive: NULL-sequence (legacy) rows always sort oldest in the window/cursor ordering, so the oldest-loaded row is only unsequenced once every sequenced row is already loaded; the unsequenced cursor can never strand sequenced rows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 18af243 commit 9986e4b

2 files changed

Lines changed: 22 additions & 5 deletions

File tree

apps/mobile/src/state/use-thread-composer-state.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,14 +113,19 @@ export function useThreadComposerState() {
113113
: null;
114114
const activityRequestKeyRef = useRef(activityRequestKey);
115115
activityRequestKeyRef.current = activityRequestKey;
116+
117+
const liveActivities = selectedThreadDetail?.activities ?? EMPTY_ACTIVITIES;
118+
// The live window's oldest activity is stable while new activities only
119+
// append; it changes when the window is re-snapshotted (reconnect) or rows are
120+
// removed (checkpoint revert), either of which can make prepended older pages
121+
// stale or gappy — so reset the lazy-load state when it (or the thread) changes.
122+
const liveOldestActivityId = liveActivities[0]?.id ?? null;
116123
useEffect(() => {
117124
setOlderActivities([]);
118125
setOlderLoaded(false);
119126
setOlderHasMore(false);
120127
setLoadingOlderActivities(false);
121-
}, [activityRequestKey]);
122-
123-
const liveActivities = selectedThreadDetail?.activities ?? EMPTY_ACTIVITIES;
128+
}, [activityRequestKey, liveOldestActivityId]);
124129
const mergedActivities = useMemo(
125130
() =>
126131
olderActivities.length > 0 ? [...olderActivities, ...liveActivities] : liveActivities,

apps/web/src/components/ChatView.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1997,14 +1997,19 @@ function ChatViewContent(props: ChatViewProps) {
19971997
: null;
19981998
const activeThreadActivityRequestKeyRef = useRef(activeThreadActivityRequestKey);
19991999
activeThreadActivityRequestKeyRef.current = activeThreadActivityRequestKey;
2000+
const liveThreadActivities = activeThread?.activities ?? EMPTY_ACTIVITIES;
2001+
// The live window's oldest activity is stable while new activities only
2002+
// append; it changes when the window is re-snapshotted (reconnect) or rows are
2003+
// removed (checkpoint revert), either of which can make prepended older pages
2004+
// stale or gappy — so reset the lazy-load state when it (or the thread) changes.
2005+
const liveOldestActivityId = liveThreadActivities[0]?.id ?? null;
20002006
useEffect(() => {
20012007
setOlderActivities([]);
20022008
setOlderLoaded(false);
20032009
setOlderHasMore(false);
20042010
setLoadingOlderActivities(false);
2005-
}, [activeThreadActivityRequestKey]);
2011+
}, [activeThreadActivityRequestKey, liveOldestActivityId]);
20062012

2007-
const liveThreadActivities = activeThread?.activities ?? EMPTY_ACTIVITIES;
20082013
const threadActivities = useMemo(
20092014
() =>
20102015
olderActivities.length > 0
@@ -2053,7 +2058,13 @@ function ChatViewContent(props: ChatViewProps) {
20532058
}
20542059
const page = result.value;
20552060
setOlderActivities((prev) => {
2061+
// Dedup against both already-loaded older pages and the live window so
2062+
// an overlap at the window boundary can't leave duplicate ids (which
2063+
// would break timeline keys and work-log derivation).
20562064
const seen = new Set(prev.map((activity) => activity.id));
2065+
for (const activity of liveThreadActivities) {
2066+
seen.add(activity.id);
2067+
}
20572068
const fresh = page.activities.filter((activity) => !seen.has(activity.id));
20582069
return [...fresh, ...prev];
20592070
});
@@ -2073,6 +2084,7 @@ function ChatViewContent(props: ChatViewProps) {
20732084
activeThreadActivityRequestKey,
20742085
hasMoreOlderActivities,
20752086
threadActivities,
2087+
liveThreadActivities,
20762088
loadThreadActivities,
20772089
]);
20782090

0 commit comments

Comments
 (0)