Lazily load conversation history: 50 most recent first, paginate on s…#252
Conversation
…croll Previously every conversation page loaded its entire event history in a single REST call (`limit: 100` plus a WebSocket `resend_all` replay), which is slow and wasteful for long-running conversations. Now: - `EventService.searchEvents` takes an options bag (`limit`, `pageId`, `sortOrder`, `timestampGte`, `timestampLt`) and returns the raw `EventPage` so callers can paginate. Both local and cloud-proxy paths forward the params. - `useConversationHistory` fetches only the most recent `INITIAL_HISTORY_PAGE_SIZE` (50) events with `sort_order='TIMESTAMP_DESC'` and reverses them to chronological order. - `useLoadOlderEvents` is a new on-demand hook that paginates older events via `timestamp__lt = <oldest known timestamp>`. `ChatInterface` triggers it when the user scrolls within 80px of the top, shows a loading spinner, and preserves the visible scroll position by adjusting `scrollTop` by the inserted height delta. - `ConversationWebSocketProvider` waits for the REST query to settle before opening the main socket, then subscribes with `resend_mode='since'` and `after_timestamp = <latest preloaded event timestamp>`, falling back to `resend_mode='all'` if REST returned no events or errored. The legacy count-based history loading detection is dropped for the main connection. - Event store gained a bulk `addEvents` action (used for the initial REST seed and for "scroll-up" pagination) that re-sorts by timestamp once at the end so prepending an older page stays O(N log N) overall. Co-authored-by: openhands <openhands@all-hands.dev>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
all-hands-bot
left a comment
There was a problem hiding this comment.
Well-structured pagination implementation with good test coverage. A few issues to address around error handling, plus this should go through eval/QA since it changes conversation loading behavior.
Code Review SummaryTaste Rating: 🟡 Acceptable - Good architectural approach with solid test coverage, but needs error handling improvements. What Works Well✅ Smart pagination strategy: Loading only the 50 most recent events first is pragmatic - solves the real problem of slow loads for long conversations without over-engineering. ✅ REST-first, then WebSocket: Waiting for the initial REST query to settle before opening the WebSocket with ✅ Comprehensive tests: New tests for ✅ Scroll position preservation: The ✅ Deduplication: Event store's Issues FoundSee inline comments for details:
Risk AssessmentKey Risk Factors:
Mitigating Factors:
Recommendation:
Verdict❌ Needs rework: Fix error handling for pagination failures, then flag for human review + eval before merging. Key Insight: The pagination architecture is sound, but production reliability requires proper error surfacing. Users need to know when "scroll up for more" fails, not just see a spinner disappear.
|
The previous gate (`conversationUserEventsExist`) hid the chat whenever the
loaded window contained no `source: 'user'` events. With the new lazy
"50 most recent" REST fetch, long agent runs between user turns can push
the original prompt out of the initial window, leaving Messages unmounted.
Without rendered messages there is no scroll container content, so
'scroll up to load older' never fires — the user appears stuck on a blank
chat.
Switch the gate to `renderableEvents.length > 0`. The empty-state
ChatSuggestions block above keeps its own `!userEventsExist &&
!hasSubstantiveAgentActions` gate, so brand-new conversations still show
suggestions instead of an empty chat.
Adds a regression test for the bug and a sibling test that the scroll
handler issues `searchEvents({ timestamp__lt })` when the user scrolls
near the top.
Co-authored-by: openhands <openhands@all-hands.dev>
|
@OpenHands address any PR feedback that has not already been addressed. resolve comments once complete |
|
I'm on it! rbren can track my progress at all-hands.dev |
all-hands-bot
left a comment
There was a problem hiding this comment.
Well-structured lazy loading implementation with good test coverage, but the three unresolved issues from the previous review are still present and should be addressed before merging.
Code Review SummaryTaste Rating: 🟡 Acceptable - Core architecture is solid but has implementation gaps Architecture AnalysisWhat's Good:
Data Flow:
[IMPROVEMENT OPPORTUNITIES]The three unresolved issues from the previous review are still present:
[TESTING GAPS]✅ Good coverage:
No additional tests needed. [RISK ASSESSMENT]Risk Factors:
Recommendation: Address the error handling gaps (#1, #2) before merging. The performance issue (#3) is worth fixing but not blocking. The breaking change (#4) should be documented in the PR description. VERDICT: KEY INSIGHT:
|
Co-authored-by: openhands <openhands@all-hands.dev>
|
OpenHands encountered an error: Request timeout after 30 seconds to https://tjzurqazzqttrice.prod-runtime.all-hands.dev/api/conversations/bb3c2b7f-974e-48ee-9224-9a7d3852459d/ask_agent See the conversation for more information. |
The browser only dispatches a 'scroll' event while `scrollTop` is
actually changing. Two paginated-history cases never triggered:
1. The user is already at `scrollTop = 0` and tries to wheel further
up (overscroll). No scroll event fires.
2. The initial 50-event window is short enough to fit on screen, so
there is no scrollbar at all and nothing to scroll.
In both cases the user had to scroll down a bit and back up to coax
loadOlder into firing.
Replace `handleScrollForPagination` with a single `maybeLoadOlder`
predicate that fires when `scrollTop <= threshold` OR
`scrollHeight <= clientHeight + threshold`, and wire it from three
sources:
- onScroll (existing path: user scrolls near the top)
- onWheel (catches overscroll-at-top: scrollTop == 0 and deltaY < 0)
- useEffect (catches no-overflow: re-runs after each rendered page so
we keep filling until the viewport overflows or the
server has no more older events)
Tests cover all three trigger paths.
Co-authored-by: openhands <openhands@all-hands.dev>
all-hands-bot
left a comment
There was a problem hiding this comment.
Well-structured lazy loading implementation with good test coverage. However, there are critical race conditions and performance issues that must be fixed before merging. Additionally, this PR changes conversation loading behavior (agent runtime behavior) and requires lightweight eval/QA before approval per repo policy.
Risk AssessmentRisk Factors:
Recommendation: Do not auto-merge. After addressing the critical issues:
Key Architectural InsightData structure choice is sound - The REST-first + WS-since pattern with timestamp-based pagination elegantly solves the "load everything upfront" problem. However, the execution has concurrency gaps: the state-based guards (
|
|
@OpenHands address the PR feedback and fix the failing gh action |
|
I'm on it! rbren can track my progress at all-hands.dev |
all-hands-bot
left a comment
There was a problem hiding this comment.
Well-structured lazy loading implementation with good test coverage. However, four unresolved issues from the previous review remain and should be addressed before merging.
The core architecture is solid: REST-first for the most recent 50 events, then WebSocket with resend_mode='since', and on-demand pagination via scroll. The bulk event insertion optimization and ref-based race guards are good improvements.
Key issues to fix:
- Missing API response validation - could cause runtime errors if server returns malformed data
- Inconsistent hasMore detection - different logic between initial load and pagination
- Potential scroll jank - position restoration may read stale DOM height
- Inefficient refetch handling - minor performance concern
See inline comments for details and suggested fixes.
|
[RISK ASSESSMENT] This PR changes the core conversation loading flow from a single blocking REST call + WebSocket replay to a paginated REST-first approach. Key risk factors:
The unresolved validation issues elevate this from LOW to MEDIUM risk. They should be fixed before merging to prevent production conversation-loading failures. VERDICT: ❌ Needs fixes: Address the three Important-level validation/consistency issues before merging. The scroll jank and refetch efficiency issues are lower priority but worth fixing. KEY INSIGHT: Solid lazy-loading architecture with good performance optimizations (bulk insert, ref-based race guards), but defensive programming is missing at the API boundary - always validate external data before spreading/iterating. |
Co-authored-by: openhands <openhands@all-hands.dev>
|
OpenHands encountered an error: Request timeout after 30 seconds to https://lrbrjkifgcmbzrwy.prod-runtime.all-hands.dev/api/conversations/700bdfac-afe9-4ce3-97c1-4b71756b386e/ask_agent See the conversation for more information. |
Co-authored-by: openhands <openhands@all-hands.dev>
|
Addressed the latest feedback in Changes:
Verification:
This comment was generated by an AI agent (OpenHands) on behalf of the user. |
all-hands-bot
left a comment
There was a problem hiding this comment.
🟢 Good taste - Clean lazy loading implementation with solid REST-first → WebSocket pattern. No technical issues found.
[RISK ASSESSMENT]
- [Overall PR]
⚠️ Risk Assessment: 🟡 MEDIUM
Changes conversation loading mechanism (REST-first with 50-event window, then WebSocket resend_mode='since') and adds scroll-based pagination. However, this is a frontend performance optimization that doesn't affect agent behavior—the agent runtime still receives all events via WebSocket as before. Good test coverage and error handling throughout.
VERDICT:
✅ Worth merging: Well-designed lazy loading that improves frontend performance without changing agent execution logic.
KEY INSIGHT:
The REST-first pattern with after_timestamp subscription elegantly avoids duplicate event streaming while maintaining real-time updates.
…croll
Previously every conversation page loaded its entire event history in a single REST call (
limit: 100plus a WebSocketresend_allreplay), which is slow and wasteful for long-running conversations.Now:
EventService.searchEventstakes an options bag (limit,pageId,sortOrder,timestampGte,timestampLt) and returns the rawEventPageso callers can paginate. Both local and cloud-proxy paths forward the params.useConversationHistoryfetches only the most recentINITIAL_HISTORY_PAGE_SIZE(50) events withsort_order='TIMESTAMP_DESC'and reverses them to chronological order.useLoadOlderEventsis a new on-demand hook that paginates older events viatimestamp__lt = <oldest known timestamp>.ChatInterfacetriggers it when the user scrolls within 80px of the top, shows a loading spinner, and preserves the visible scroll position by adjustingscrollTopby the inserted height delta.ConversationWebSocketProviderwaits for the REST query to settle before opening the main socket, then subscribes withresend_mode='since'andafter_timestamp = <latest preloaded event timestamp>, falling back toresend_mode='all'if REST returned no events or errored. The legacy count-based history loading detection is dropped for the main connection.addEventsaction (used for the initial REST seed and for "scroll-up" pagination) that re-sorts by timestamp once at the end so prepending an older page stays O(N log N) overall.Why
Summary
Issue Number
How to Test
Video/Screenshots
Type
Notes