feat(websocket): add after_timestamp filter for bi-directional event loading#1880
feat(websocket): add after_timestamp filter for bi-directional event loading#1880
Conversation
all-hands-bot
left a comment
There was a problem hiding this comment.
Overall solid implementation! The feature is well-tested and maintains backward compatibility. Main concern is timezone handling - the parameter should enforce or document timezone-aware datetimes to avoid potential bugs.
|
@OpenHands Do a /codereview-roasted on this PR. When you’re done, look at the linked issue and tell WDYT. Post your feedback directly as a comment on github here. Use smolpaws. Note that it will be rendered as markdown. |
|
I'm on it! enyst can track my progress at all-hands.dev |
|
Taste Rating: 🟡 Acceptable — small change, but there’s a correctness landmine hiding in the timestamp handling. Linus-Style Analysis[CRITICAL ISSUES]
[IMPROVEMENT OPPORTUNITIES]
[STYLE NOTES]
VERDICT: ✅ Worth merging once timestamp normalization is addressed. Otherwise you’re adding a correctness bug under load. KEY INSIGHT: If timestamps aren’t normalized, the filter is gambling on string ordering — that’s not a protocol, it’s a coincidence. WDYT on OpenHands/OpenHands#12705The bi-directional loading plan is solid and pragmatic — REST for bulk history + WebSocket for tail is the right shape. Two gotchas to keep in mind:
Overall: good direction, just nail the timestamp semantics so you don’t ship a subtle “missing events” bug. |
This comment was marked as duplicate.
This comment was marked as duplicate.
|
Thanks for the detailed review! I've pushed fixes that address the timezone handling concerns. Let me explain the architecture and our decisions. Background: How Event Timestamps WorkEvent timestamps are defined in # openhands-sdk/openhands/sdk/event/base.py
timestamp: str = Field(
default_factory=lambda: datetime.now().isoformat(),
) # consistent with V1Key points:
The agent-server's filtering code must work with this existing format. The ProblemThe original implementation used string comparison for timestamp filtering: if event.timestamp < timestamp_gte_str: # string comparisonThis breaks when ISO strings have different timezone suffixes:
These represent potentially different moments but don't compare correctly as strings. Our SolutionWe had two options to enable proper datetime comparison:
We chose Option A because:
Changes Made
API Contract
This handles timezones correctly - a client can pass UTC and it will be properly converted to server time before comparison. No Breaking ChangesVerified that OpenHands/OpenHands frontend doesn't use |
|
Looks like there are a few issues preventing this PR from being merged!
If you'd like me to help, just leave a comment, like Feel free to include any additional details that might help me get this PR into a better state. You can manage your notification settings |
|
[Automatic Post]: It has been a while since there was any activity on this PR. @jpshackelford, are you still working on it? If so, please go ahead, if not then please request review, close it, or request that someone else follow up. |
|
[automated message] @neubig assigned for review according to git blame |
|
[Automatic Post]: This PR seems to be currently waiting for review. @neubig, could you please take a look when you have a chance? |
|
[Automatic Post]: It has been a while since there was any activity on this PR. @jpshackelford, are you still working on it? If so, please go ahead, if not then please request review, close it, or request that someone else follow up. |
2 similar comments
|
[Automatic Post]: It has been a while since there was any activity on this PR. @jpshackelford, are you still working on it? If so, please go ahead, if not then please request review, close it, or request that someone else follow up. |
|
[Automatic Post]: It has been a while since there was any activity on this PR. @jpshackelford, are you still working on it? If so, please go ahead, if not then please request review, close it, or request that someone else follow up. |
…loading Add an `after_timestamp` query parameter to the WebSocket events endpoint that filters events during resend_all to only return events with timestamps >= the specified value. This enables efficient bi-directional loading where: - REST API fetches historical events (paginated backwards from newest) - WebSocket handles real-time events after a specific timestamp The underlying search_events already supports timestamp__gte filtering, so this change simply exposes that capability through the WebSocket API. Resolves: OpenHands/OpenHands#12705 (agent-server prerequisite) Co-authored-by: openhands <openhands@all-hands.dev>
Address review feedback on timezone handling: - WebSocket endpoint now normalizes tz-aware datetimes (matches REST) - event_service.py uses datetime comparison instead of string comparison - Updated tests to reflect the new timezone contract Co-authored-by: openhands <openhands@all-hands.dev>
Complete the timezone handling fix by ensuring normalized datetimes are naive (no tzinfo), matching the format of stored event timestamps. This allows proper datetime comparison in event_service without TypeError. Co-authored-by: openhands <openhands@all-hands.dev>
- Add _make_timestamp_filter() helper to eliminate code duplication - Convert datetime filters to ISO strings once (O(1) vs O(n) parsing) - Add unit tests for normalize_datetime_to_server_timezone() - Add unit tests for _make_timestamp_filter() helper - Add behavioral tests that verify actual filtering (not just mock calls) - Add warning log when after_timestamp provided without resend_all - Add Query description for after_timestamp in OpenAPI docs Co-authored-by: openhands <openhands@all-hands.dev>
Address review comments: - Convert TestNormalizeDatetimeToServerTimezone class to standalone functions - Convert TestAfterTimestampFiltering class to standalone functions - Remove TestAfterTimestampFilteringBehavioral class (duplicative with event_service tests) - Remove TestWebSocketSubscriber, TestWebSocketDisconnectHandling, TestResendAllFunctionality classes - Move imports to module level (datetime, cast, EventPage, Event) - Reduce test count from 35+ to 11 tests for websocket functionality Co-authored-by: openhands <openhands@all-hands.dev>
00985c4 to
14d9f69
Compare
Python API breakage checks — ✅ PASSEDResult: ✅ PASSED |
REST API breakage checks (OpenAPI) — ✅ PASSEDResult: ✅ PASSED |
Summary: API Breakage Checks✅ API breakage checks (Griffe) - PASSED
|
|
@neubig Ready for final review |
|
@jpshackelford , thanks! When you want to re-request my review, could you press the "cycle" button next to my name under reviewers? That'll put it back on my review queue, and I'm better at checking that than my github messages (which are often a bit noisy). I'll click it for myself for now. |
Implements the API change discussed in PR review: - Add new resend_mode parameter with values: 'all', 'since', or None - Deprecate resend_all parameter (hidden from OpenAPI schema, logs warning when used) - resend_mode='all' sends all events (replaces resend_all=True) - resend_mode='since' with after_timestamp sends events since timestamp - resend_mode=None (default) subscribes without resending Migration path: - Old: ?resend_all=true - New: ?resend_mode=all - Old: ?resend_all=true&after_timestamp=... - New: ?resend_mode=since&after_timestamp=... Backward compatibility: - resend_all=True still works (with deprecation warning) - resend_all=False works as before (no resend) - If resend_mode is set, it takes precedence over resend_all Co-authored-by: openhands <openhands@all-hands.dev>
Implemented:
|
| Phase | API Surface | Breaking? |
|---|---|---|
| Now | resend_mode, after_timestamp, resend_all (hidden) |
No |
| Deprecation period | Warn in logs when resend_all used |
No |
| Future major release | Remove resend_all |
Yes |
| Final | resend_mode, after_timestamp |
- |
Frontend Migration (when ready)
- { resend_all: true }
+ { resend_mode: 'all' }Commit: 2d7f74b
Co-authored-by: openhands <openhands@all-hands.dev>
all-hands-bot
left a comment
There was a problem hiding this comment.
Taste Rating: 🟢 Good taste
Clean implementation solving a real problem (bi-directional event loading). Key strengths:
- Data structure handling: Correctly normalizes timezone-aware datetimes to naive server local time for comparison with stored events
- Simplicity: No excessive nesting, clear branching logic for resend modes
- Backward compatibility: Deprecated
resend_allgracefully with warnings, newresend_modeAPI is cleaner - Testing: Comprehensive coverage testing real behavior (not mocks) - timezone normalization, all modes, backward compat
No critical or important issues found. The approach is pragmatic and maintainable.
VERDICT: ✅ Worth merging - solid engineering, well-tested, maintains backward compatibility.
enyst
left a comment
There was a problem hiding this comment.
Thank you for this! Let's take it in.
Summary
Add an
after_timestampquery parameter to the WebSocket events endpoint that filters events duringresend_allto only return events with timestamps >= the specified value.Background
This is a prerequisite for the bi-directional event loading optimization described in OpenHands/OpenHands#12705.
Changes
after_timestamp: datetime | Nonequery parameter to/sockets/events/{conversation_id}WebSocket endpointresend_all=Trueandafter_timestampis provided, only events withtimestamp >= after_timestampare resentUse Case
This enables efficient bi-directional loading where:
The underlying
search_eventsalready supportstimestamp__gtefiltering, so this change simply exposes that capability through the WebSocket API.Testing
TestAfterTimestampFilteringclasstest_resend_all_true_resends_eventsto verify the new parameter passingRelated Issue
Resolves: OpenHands/OpenHands#12705 (agent-server prerequisite)
Agent Server images for this PR
• GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server
Variants & Base Images
eclipse-temurin:17-jdknikolaik/python-nodejs:python3.13-nodejs22golang:1.21-bookwormPull (multi-arch manifest)
# Each variant is a multi-arch manifest supporting both amd64 and arm64 docker pull ghcr.io/openhands/agent-server:bce2fa7-pythonRun
All tags pushed for this build
About Multi-Architecture Support
bce2fa7-python) is a multi-arch manifest supporting both amd64 and arm64bce2fa7-python-amd64) are also available if needed