Bugfix refresh loses unanswered message - #574
Open
QuanCheng-QC wants to merge 4 commits into
Open
Conversation
Refreshing the page while an agent was still working could make the just-sent question disappear. Two causes were fixed. History loading counted agent thinking/status/todos events against the 50-event window, so a busy agent pushed the user's own message out of the first page. GET /v1/events now supports exclude_message_types (wired into both poll cache keys) and the frontend uses it for channel history, DM history and infinite scroll. The forward poll still fetches intermediate events, so the current-steps display is unaffected. nginx had no client_max_body_size, so its 1MB default rejected larger image uploads with 413 before the backend's 50MB limit ever applied. Sending many screenshots failed silently and the whole message was lost. The send path no longer swallows failures. It shows a toast, restores the typed text as the thread draft, and validates file size client-side before uploading.
Run one unfiltered catch-up poll after history hydration. SSE only carries events published after the connection opens and history now excludes step events, so without the catch-up poll the steps emitted before a refresh never loaded and a working agent looked idle. Share one filter-hash builder between poll_events and the poll-cache invalidation. The exclude_message_types param joins the hash only when set, so the enumerated adapter patterns keep their legacy hashes and head/at-head keys stay invalidatable. Covered by regression tests. Scope the nginx body limit to the /v1/files upload route instead of the whole server, and raise it to 52m so multipart overhead cannot 413 a file of exactly 50MB. The backend still enforces the precise per-file limit.
Merge incoming messages sorted by timestamp with the id as tiebreaker
instead of appending. The catch-up poll and the SSE stream run
concurrently, so an older status batch could land after a newer
SSE-delivered reply, become the trailing message, and make the UI treat
a finished agent as still working. The newest-message cursor now only
advances chronologically, and the poll loop paginates on a local cursor
so a concurrent SSE jump cannot skip the batches it is backfilling.
Run the catch-up poll only when history hydration succeeded. After a
failed history request the cursor is null and the poll loop would page
through the channel's entire event log from the first message.
Use exact-match nginx locations for the two upload endpoints so
GET /v1/files/{id} and other file routes keep the default body limit.
POST /v1/files/base64 needs its own location with ~4/3 headroom for the
base64 encoding overhead, which the review's prefix-match concern did
not cover.
Revert the incidental next-env.d.ts change from the previous commit —
it was a next build artifact, not an intended edit.
Merge history hydration into live state instead of replacing it. The SSE stream opens alongside the history request, so a reply delivered during hydration was dropped by the replace and the cursor rolled back; a failed catch-up would then never recover it since SSE does not replay. The empty-history branch likewise no longer clears state the SSE stream already delivered, and the newest cursor only moves through bumpNewest so it stays monotonic. Skip the catch-up poll when no cursor exists after hydration. A successful history response over a channel containing only excluded step events left the cursor null, and the poll loop would page through the channel's entire event log from the beginning. Extract the merge helpers into lib/message-merge.ts and add vitest with unit tests covering the out-of-order status-after-reply scenario, the hydration/SSE interleave, dedup, and the null-timestamp comparator. The frontend previously had no test setup at all; hook-level timing tests around the polling loop would additionally need jsdom and API mocks and are left for follow-up.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Sending a complex task (e.g. 22 screenshots + instructions) and refreshing the page while the agent was still thinking made the just-sent question disappear. Two independent defects combined:
workspace.messageevents, but agent intermediate output (thinking/status/todos) shares that event type — a busy agent pushes the user's message out of the first page. The message was persisted; it just never loaded back.client_max_body_sizewas unset, so larger screenshot uploads were 413'd before reaching the backend (which allows 50MB). The frontend swallowed the failure silently and cleared the composer, losing the entire message.Changes
Backend
GET /v1/eventssupportsexclude_message_types(filterspayload.message_type; events without one are always kept)_poll_filter_hashbetween poll-key construction and cache invalidation — the param joins the hash only when set, so legacy adapter-poll hashes stay invalidatableFrontend
mergeMessageswith a monotonic newest-cursor — concurrent arrival order can no longer reorder messages or strand the UI on "agent working"nginx
= /v1/filesat 52m (multipart overhead headroom) and= /v1/files/base64at 70m (~4/3 base64 overhead); other routes keep the default limitTesting
tests/test_events.py(exclusion filtering, limit-window regression, cache-key consistency) pass; remaining local failures are pre-existing env issues (no Postgres), identical on clean developnpm test(6 vitest cases),tsc --noEmit,next buildall pass