@@ -68,19 +68,15 @@ export function useTimeline(
6868 if ( ! sessionId ) {
6969 setEvents ( [ ] ) ;
7070 setLoading ( false ) ;
71- setLoadingOlder ( false ) ;
72- loadingOlderRef . current = false ;
73- olderRequestIdRef . current = null ;
71+ resetOlderState ( ) ;
7472 epochRef . current = 0 ;
7573 seqRef . current = 0 ;
7674 historyLoadedRef . current = null ;
7775 return ;
7876 }
7977
8078 setRefreshing ( false ) ;
81- setLoadingOlder ( false ) ;
82- loadingOlderRef . current = false ;
83- olderRequestIdRef . current = null ;
79+ resetOlderState ( ) ;
8480 historyLoadedRef . current = null ;
8581
8682 let cancelled = false ;
@@ -165,15 +161,26 @@ export function useTimeline(
165161 } ) ;
166162 } , [ sessionId ] ) ;
167163
164+ const olderTimeoutRef = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
165+ const resetOlderState = useCallback ( ( ) => {
166+ loadingOlderRef . current = false ;
167+ olderRequestIdRef . current = null ;
168+ if ( olderTimeoutRef . current ) { clearTimeout ( olderTimeoutRef . current ) ; olderTimeoutRef . current = null ; }
169+ setLoadingOlder ( false ) ;
170+ } , [ ] ) ;
171+
168172 // Load older events (backward pagination)
169173 const loadOlderEvents = useCallback ( ( ) => {
170174 if ( ! ws ?. connected || ! sessionId || loadingOlderRef . current ) return ;
171175 const cached = eventsCache . get ( sessionId ) ;
172176 if ( ! cached || cached . length === 0 ) return ;
173177 const oldestTs = Math . min ( ...cached . map ( ( e ) => e . ts ) ) ;
174- loadingOlderRef . current = true ; // Synchronous guard — prevents duplicate requests from rapid clicks
178+ loadingOlderRef . current = true ;
175179 setLoadingOlder ( true ) ;
176180 olderRequestIdRef . current = ws . sendTimelineHistoryRequest ( sessionId , 500 , undefined , oldestTs ) ;
181+ // Timeout: if response never arrives (packet loss, disconnect), reset after 10s
182+ if ( olderTimeoutRef . current ) clearTimeout ( olderTimeoutRef . current ) ;
183+ olderTimeoutRef . current = setTimeout ( resetOlderState , 10_000 ) ;
177184 } , [ ws , sessionId , loadingOlder ] ) ;
178185
179186 // Append a single event, dedup by eventId
@@ -273,13 +280,11 @@ export function useTimeline(
273280
274281 // Handle backward pagination response
275282 if ( msg . requestId && msg . requestId === olderRequestIdRef . current ) {
276- olderRequestIdRef . current = null ;
277- loadingOlderRef . current = false ;
283+ resetOlderState ( ) ;
278284 if ( msg . events . length > 0 ) {
279285 mergeEvents ( msg . events ) ;
280286 sharedDb ?. putEvents ( msg . events ) . catch ( ( ) => { } ) ;
281287 }
282- setLoadingOlder ( false ) ;
283288 return ;
284289 }
285290
@@ -331,6 +336,10 @@ export function useTimeline(
331336 historyRequestIdRef . current = ws . sendTimelineHistoryRequest ( sessionId , 500 , afterTs ) ;
332337 }
333338 }
339+ // ── Browser WS disconnected: reset in-flight pagination to prevent stuck state ──
340+ if ( msg . type === 'session.event' && ( msg as { event : string } ) . event === 'disconnected' ) {
341+ if ( loadingOlderRef . current ) resetOlderState ( ) ;
342+ }
334343 // ── Browser WS reconnected: fill gaps since last seen seq ──
335344 if ( msg . type === 'session.event' && ( msg as { event : string } ) . event === 'connected' ) {
336345 if ( ws && sessionId && epochRef . current > 0 ) {
0 commit comments